From ae6a7145733afbdac4fc050d08c2645161d95785 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Wed, 17 Jan 2024 00:12:57 +0100 Subject: [PATCH 01/10] First implementation of Event system Signed-off-by: Francis Bouvier --- src/apiweb.zig | 2 ++ src/dom/event_target.zig | 58 ++++++++++++++++++++++++++++++++++++++++ src/events/event.zig | 29 ++++++++++++++++++++ src/netsurf.zig | 42 +++++++++++++++++++++++++++++ src/run_tests.zig | 2 ++ 5 files changed, 133 insertions(+) create mode 100644 src/events/event.zig diff --git a/src/apiweb.zig b/src/apiweb.zig index 08f4c3f5..40cc8779 100644 --- a/src/apiweb.zig +++ b/src/apiweb.zig @@ -4,6 +4,7 @@ const Console = @import("jsruntime").Console; const DOM = @import("dom/dom.zig"); const HTML = @import("html/html.zig"); +const Events = @import("events/event.zig"); pub const HTMLDocument = @import("html/document.zig").HTMLDocument; @@ -11,5 +12,6 @@ pub const HTMLDocument = @import("html/document.zig").HTMLDocument; pub const Interfaces = generate.Tuple(.{ Console, DOM.Interfaces, + Events.Interfaces, HTML.Interfaces, }); diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig index 9058f9c0..03ae8975 100644 --- a/src/dom/event_target.zig +++ b/src/dom/event_target.zig @@ -1,3 +1,10 @@ +const std = @import("std"); + +const jsruntime = @import("jsruntime"); +const Callback = jsruntime.Callback; +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; + const parser = @import("../netsurf.zig"); const DOMException = @import("exceptions.zig").DOMException; @@ -6,4 +13,55 @@ pub const EventTarget = struct { pub const Self = parser.EventTarget; pub const Exception = DOMException; pub const mem_guarantied = true; + + // JS funcs + // -------- + + const js_handler = struct { + fn handle(event: ?*parser.Event, data: ?*anyopaque) callconv(.C) void { + const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data.?); + const func = @as(*Callback, @ptrCast(ptr)); + func.call(.{event}) catch unreachable; + // NOTE: we can not call func.deinit here + // b/c the handler can be called several times + // as the event goes through the ancestors + // TODO: check the event phase to call func.deinit and free func + } + }.handle; + + pub fn _addEventListener( + self: *parser.EventTarget, + alloc: std.mem.Allocator, + eventType: []const u8, + cbk: Callback, + ) !void { + // TODO: when can we free this allocation? + const cbk_ptr = try alloc.create(Callback); + cbk_ptr.* = cbk; + const func = @as(*anyopaque, @ptrCast(cbk_ptr)); + try parser.eventTargetAddEventListener(self, eventType, func, js_handler); + } + + pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool { + return try parser.eventTargetDispatchEvent(self, event); + } + + pub fn deinit(_: *parser.EventTarget, _: std.mem.Allocator) void {} }; + +// Tests +// ----- + +pub fn testExecFn( + _: std.mem.Allocator, + js_env: *jsruntime.Env, +) anyerror!void { + var basic = [_]Case{ + .{ .src = "let event = new Event('myEvent')", .ex = "undefined" }, + .{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, + .{ .src = "var nb = 0; content.addEventListener('myEvent', function(event) {nb ++;})", .ex = "undefined" }, + .{ .src = "content.dispatchEvent(event)", .ex = "true" }, + .{ .src = "nb", .ex = "2" }, // 2 because the callback is called twice + }; + try checkCases(js_env, &basic); +} diff --git a/src/events/event.zig b/src/events/event.zig new file mode 100644 index 00000000..a336a700 --- /dev/null +++ b/src/events/event.zig @@ -0,0 +1,29 @@ +const std = @import("std"); + +const generate = @import("../generate.zig"); + +const parser = @import("../netsurf.zig"); + +const DOMException = @import("../dom/exceptions.zig").DOMException; + +pub const Event = struct { + pub const Self = parser.Event; + pub const Exception = DOMException; + pub const mem_guarantied = true; + + // JS funcs + // -------- + + pub fn constructor(eventType: []const u8) !*parser.Event { + const event = try parser.eventCreate(); + try parser.eventInit(event, eventType); + return event; + } +}; + +// Event interfaces +pub const Interfaces = generate.Tuple(.{ + Event, +}); +const Generated = generate.Union.compile(Interfaces); +pub const Union = Generated._union; diff --git a/src/netsurf.zig b/src/netsurf.zig index 7d5802b1..b8d4d7e2 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -341,9 +341,51 @@ fn DOMErr(except: DOMException) DOMError!void { }; } +// Event +pub const Event = c.dom_event; +pub const EventHandler = fn (?*Event, ?*anyopaque) callconv(.C) void; + +pub fn eventCreate() !*Event { + var evt: ?*Event = undefined; + const err = c._dom_event_create(&evt); + try DOMErr(err); + return evt.?; +} + +pub fn eventInit(evt: *Event, eventType: []const u8) !void { + const s = try strFromData(eventType); + const err = c._dom_event_init(evt, s, false, false); + try DOMErr(err); +} + // EventTarget pub const EventTarget = c.dom_event_target; +fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable { + return getVtable(c.dom_event_target_vtable, EventTarget, et); +} + +pub fn eventTargetAddEventListener( + et: *EventTarget, + eventType: []const u8, + data: ?*anyopaque, + comptime handler: EventHandler, +) !void { + const s = try strFromData(eventType); + var listener: ?*c.dom_event_listener = undefined; + const errLst = c.dom_event_listener_create(handler, data, &listener); + try DOMErr(errLst); + const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, true); + try DOMErr(err); +} + +pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool { + var res: bool = undefined; + const err = eventTargetVtable(et).dispatch_event.?(et, event, &res); + try DOMErr(err); + return res; +} + // NodeType pub const NodeType = enum(u4) { diff --git a/src/run_tests.zig b/src/run_tests.zig index 3a8fc499..f54f5186 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -20,6 +20,7 @@ const NamedNodeMapExecFn = @import("dom/namednodemap.zig").testExecFn; const DOMTokenListExecFn = @import("dom/token_list.zig").testExecFn; const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn; const AttrTestExecFn = @import("dom/attribute.zig").testExecFn; +const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn; pub const Types = jsruntime.reflect(apiweb.Interfaces); @@ -73,6 +74,7 @@ fn testsAllExecFn( DOMTokenListExecFn, NodeListTestExecFn, AttrTestExecFn, + EventTargetTestExecFn, }; inline for (testFns) |testFn| { From 86cc3d25dcb0b10e947065ea8de38bc3411d180e Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Wed, 17 Jan 2024 15:29:48 +0100 Subject: [PATCH 02/10] Move some details implementation from EventTarget to the parser Signed-off-by: Francis Bouvier --- src/dom/event_target.zig | 15 +-------------- src/netsurf.zig | 24 ++++++++++++++++++++---- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig index 03ae8975..9a49585d 100644 --- a/src/dom/event_target.zig +++ b/src/dom/event_target.zig @@ -17,18 +17,6 @@ pub const EventTarget = struct { // JS funcs // -------- - const js_handler = struct { - fn handle(event: ?*parser.Event, data: ?*anyopaque) callconv(.C) void { - const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data.?); - const func = @as(*Callback, @ptrCast(ptr)); - func.call(.{event}) catch unreachable; - // NOTE: we can not call func.deinit here - // b/c the handler can be called several times - // as the event goes through the ancestors - // TODO: check the event phase to call func.deinit and free func - } - }.handle; - pub fn _addEventListener( self: *parser.EventTarget, alloc: std.mem.Allocator, @@ -38,8 +26,7 @@ pub const EventTarget = struct { // TODO: when can we free this allocation? const cbk_ptr = try alloc.create(Callback); cbk_ptr.* = cbk; - const func = @as(*anyopaque, @ptrCast(cbk_ptr)); - try parser.eventTargetAddEventListener(self, eventType, func, js_handler); + try parser.eventTargetAddEventListener(self, eventType, cbk_ptr); } pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool { diff --git a/src/netsurf.zig b/src/netsurf.zig index b8d4d7e2..863ccc1d 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -5,6 +5,8 @@ const c = @cImport({ @cInclude("dom/bindings/hubbub/parser.h"); }); +const Callback = @import("jsruntime").Callback; + // Vtable // ------ @@ -343,7 +345,6 @@ fn DOMErr(except: DOMException) DOMError!void { // Event pub const Event = c.dom_event; -pub const EventHandler = fn (?*Event, ?*anyopaque) callconv(.C) void; pub fn eventCreate() !*Event { var evt: ?*Event = undefined; @@ -358,6 +359,21 @@ pub fn eventInit(evt: *Event, eventType: []const u8) !void { try DOMErr(err); } +// EventHandler +pub const EventHandler = fn (?*Event, ?*anyopaque) callconv(.C) void; + +const event_handler = struct { + fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void { + const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data.?); + const func = @as(*Callback, @ptrCast(ptr)); + func.call(.{event}) catch unreachable; + // NOTE: we can not call func.deinit here + // b/c the handler can be called several times + // as the event goes through the ancestors + // TODO: check the event phase to call func.deinit and free func + } +}.handle; + // EventTarget pub const EventTarget = c.dom_event_target; @@ -368,12 +384,12 @@ fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable { pub fn eventTargetAddEventListener( et: *EventTarget, eventType: []const u8, - data: ?*anyopaque, - comptime handler: EventHandler, + cbk_ptr: *Callback, ) !void { const s = try strFromData(eventType); + const ctx = @as(*anyopaque, @ptrCast(cbk_ptr)); var listener: ?*c.dom_event_listener = undefined; - const errLst = c.dom_event_listener_create(handler, data, &listener); + const errLst = c.dom_event_listener_create(event_handler, ctx, &listener); try DOMErr(errLst); const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, true); try DOMErr(err); From 2faf584d367752b98d42ea11c1e9c0ea2df20476 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Thu, 18 Jan 2024 22:00:36 +0100 Subject: [PATCH 03/10] Updates, fix and Event properties Signed-off-by: Francis Bouvier --- src/dom/event_target.zig | 36 +++++++++++++-- src/events/event.zig | 73 ++++++++++++++++++++++++++++-- src/netsurf.zig | 98 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 194 insertions(+), 13 deletions(-) diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig index 9a49585d..6fbfbd47 100644 --- a/src/dom/event_target.zig +++ b/src/dom/event_target.zig @@ -8,12 +8,23 @@ const checkCases = jsruntime.test_utils.checkCases; const parser = @import("../netsurf.zig"); const DOMException = @import("exceptions.zig").DOMException; +const Nod = @import("node.zig"); +// EventTarget interfaces +pub const Union = Nod.Union; + +// EventTarget implementation pub const EventTarget = struct { pub const Self = parser.EventTarget; pub const Exception = DOMException; pub const mem_guarantied = true; + pub fn toInterface(et: *parser.EventTarget) !Union { + // NOTE: for now we state that all EventTarget are Nodes + // TODO: handle other types (eg. Window) + return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et))); + } + // JS funcs // -------- @@ -22,11 +33,14 @@ pub const EventTarget = struct { alloc: std.mem.Allocator, eventType: []const u8, cbk: Callback, + capture: ?bool, + // TODO: hanle EventListenerOptions + // see #https://github.com/lightpanda-io/jsruntime-lib/issues/114 ) !void { // TODO: when can we free this allocation? const cbk_ptr = try alloc.create(Callback); cbk_ptr.* = cbk; - try parser.eventTargetAddEventListener(self, eventType, cbk_ptr); + try parser.eventTargetAddEventListener(self, eventType, cbk_ptr, capture orelse false); } pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool { @@ -46,9 +60,25 @@ pub fn testExecFn( var basic = [_]Case{ .{ .src = "let event = new Event('myEvent')", .ex = "undefined" }, .{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, - .{ .src = "var nb = 0; content.addEventListener('myEvent', function(event) {nb ++;})", .ex = "undefined" }, + .{ .src = + \\var nb = 0; + \\var evt = undefined; + \\var phase = undefined; + \\var cur = undefined; + \\content.addEventListener('myEvent', + \\function(event) { + \\evt = event; + \\phase = event.eventPhase; + \\cur = event.currentTarget; + \\nb ++; + \\}) + , .ex = "undefined" }, .{ .src = "content.dispatchEvent(event)", .ex = "true" }, - .{ .src = "nb", .ex = "2" }, // 2 because the callback is called twice + .{ .src = "nb", .ex = "1" }, + .{ .src = "evt instanceof Event", .ex = "true" }, + .{ .src = "evt.type", .ex = "myEvent" }, + .{ .src = "phase", .ex = "2" }, + .{ .src = "cur.localName", .ex = "div" }, }; try checkCases(js_env, &basic); } diff --git a/src/events/event.zig b/src/events/event.zig index a336a700..89576cbb 100644 --- a/src/events/event.zig +++ b/src/events/event.zig @@ -5,20 +5,85 @@ const generate = @import("../generate.zig"); const parser = @import("../netsurf.zig"); const DOMException = @import("../dom/exceptions.zig").DOMException; +const EventTarget = @import("../dom/event_target.zig").EventTarget; +const EventTargetUnion = @import("../dom/event_target.zig").Union; +// https://dom.spec.whatwg.org/#event pub const Event = struct { pub const Self = parser.Event; pub const Exception = DOMException; pub const mem_guarantied = true; - // JS funcs - // -------- + pub const EventInit = parser.EventInit; - pub fn constructor(eventType: []const u8) !*parser.Event { + // JS + // -- + + pub const _CAPTURING_PHASE = 1; + pub const _AT_TARGET = 2; + pub const _BUBBLING_PHASE = 3; + + pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event { const event = try parser.eventCreate(); - try parser.eventInit(event, eventType); + try parser.eventInit(event, eventType, opts orelse EventInit{}); return event; } + + // Getters + + pub fn get_type(self: *parser.Event) ![]const u8 { + return try parser.eventType(self); + } + + pub fn get_target(self: *parser.Event) !?EventTargetUnion { + const et = try parser.eventTarget(self); + if (et == null) return null; + return try EventTarget.toInterface(et.?); + } + + pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion { + const et = try parser.eventCurrentTarget(self); + if (et == null) return null; + return try EventTarget.toInterface(et.?); + } + + pub fn get_eventPhase(self: *parser.Event) !u8 { + return try parser.eventPhase(self); + } + + pub fn get_bubbles(self: *parser.Event) !bool { + return try parser.eventBubbles(self); + } + + pub fn get_cancelable(self: *parser.Event) !bool { + return try parser.eventCancelable(self); + } + + pub fn get_defaultPrevented(self: *parser.Event) !bool { + return try parser.eventDefaultPrevented(self); + } + + pub fn get_isTrusted(self: *parser.Event) !bool { + return try parser.eventIsTrusted(self); + } + + pub fn get_timestamp(self: *parser.Event) !u32 { + return try parser.eventTimestamp(self); + } + + // Methods + + pub fn get_stopPropagation(self: *parser.Event) !void { + return try parser.eventStopPropagation(self); + } + + pub fn get_stopImmediatePropagation(self: *parser.Event) !void { + return try parser.eventStopImmediatePropagation(self); + } + + pub fn get_preventDefault(self: *parser.Event) !void { + return try parser.eventPreventDefault(self); + } }; // Event interfaces diff --git a/src/netsurf.zig b/src/netsurf.zig index 863ccc1d..b609768b 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -353,9 +353,93 @@ pub fn eventCreate() !*Event { return evt.?; } -pub fn eventInit(evt: *Event, eventType: []const u8) !void { - const s = try strFromData(eventType); - const err = c._dom_event_init(evt, s, false, false); +pub const EventInit = struct { + bubbles: bool = false, + cancelable: bool = false, + composed: bool = false, +}; + +pub fn eventInit(evt: *Event, typ: []const u8, opts: EventInit) !void { + const s = try strFromData(typ); + const err = c._dom_event_init(evt, s, opts.bubbles, opts.cancelable); + try DOMErr(err); +} + +pub fn eventType(evt: *Event) ![]const u8 { + var s: ?*String = undefined; + const err = c._dom_event_get_type(evt, &s); + try DOMErr(err); + return strToData(s.?); +} + +pub fn eventTarget(evt: *Event) !?*EventTarget { + var et: ?*EventTarget = undefined; + const err = c._dom_event_get_target(evt, &et); + try DOMErr(err); + return et; +} + +pub fn eventCurrentTarget(evt: *Event) !?*EventTarget { + var et: ?*EventTarget = undefined; + const err = c._dom_event_get_current_target(evt, &et); + try DOMErr(err); + return et; +} + +pub fn eventPhase(evt: *Event) !u8 { + var phase: c.dom_event_flow_phase = undefined; + const err = c._dom_event_get_event_phase(evt, &phase); + try DOMErr(err); + return @as(u8, @intCast(phase)); +} + +pub fn eventBubbles(evt: *Event) !bool { + var res: bool = undefined; + const err = c._dom_event_get_bubbles(evt, &res); + try DOMErr(err); + return res; +} + +pub fn eventCancelable(evt: *Event) !bool { + var res: bool = undefined; + const err = c._dom_event_get_cancelable(evt, &res); + try DOMErr(err); + return res; +} + +pub fn eventDefaultPrevented(evt: *Event) !bool { + var res: bool = undefined; + const err = c._dom_event_is_default_prevented(evt, &res); + try DOMErr(err); + return res; +} + +pub fn eventIsTrusted(evt: *Event) !bool { + var res: bool = undefined; + const err = c._dom_event_get_is_trusted(evt, &res); + try DOMErr(err); + return res; +} + +pub fn eventTimestamp(evt: *Event) !u32 { + var ts: c_uint = undefined; + const err = c._dom_event_get_timestamp(evt, &ts); + try DOMErr(err); + return @as(u32, @intCast(ts)); +} + +pub fn eventStopPropagation(evt: *Event) !void { + const err = c._dom_event_stop_propagation(evt); + try DOMErr(err); +} + +pub fn eventStopImmediatePropagation(evt: *Event) !void { + const err = c._dom_event_stop_immediate_propagation(evt); + try DOMErr(err); +} + +pub fn eventPreventDefault(evt: *Event) !void { + const err = c._dom_event_prevent_default(evt); try DOMErr(err); } @@ -383,20 +467,22 @@ fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable { pub fn eventTargetAddEventListener( et: *EventTarget, - eventType: []const u8, + typ: []const u8, cbk_ptr: *Callback, + capture: bool, ) !void { - const s = try strFromData(eventType); + const s = try strFromData(typ); const ctx = @as(*anyopaque, @ptrCast(cbk_ptr)); var listener: ?*c.dom_event_listener = undefined; const errLst = c.dom_event_listener_create(event_handler, ctx, &listener); try DOMErr(errLst); - const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, true); + const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture); try DOMErr(err); } pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool { var res: bool = undefined; + // const err = c.dom_event_target_dispatch_event(et, event, &res); const err = eventTargetVtable(et).dispatch_event.?(et, event, &res); try DOMErr(err); return res; From da0a54a0df36d2ebbb8baeca0651292e6d17acf5 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Fri, 19 Jan 2024 13:16:40 +0100 Subject: [PATCH 04/10] Add more tests cases for event capture/target/bubbles phases Signed-off-by: Francis Bouvier --- src/dom/event_target.zig | 79 ++++++++++++++++++++++++++++++++++------ 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig index 6fbfbd47..05fd3f35 100644 --- a/src/dom/event_target.zig +++ b/src/dom/event_target.zig @@ -57,28 +57,83 @@ pub fn testExecFn( _: std.mem.Allocator, js_env: *jsruntime.Env, ) anyerror!void { - var basic = [_]Case{ - .{ .src = "let event = new Event('myEvent')", .ex = "undefined" }, + var common = [_]Case{ .{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, + .{ .src = "let para = document.getElementById('para')", .ex = "undefined" }, .{ .src = - \\var nb = 0; - \\var evt = undefined; - \\var phase = undefined; - \\var cur = undefined; - \\content.addEventListener('myEvent', - \\function(event) { + \\var nb = 0; var evt; var phase; var cur; + \\function cbk(event) { \\evt = event; \\phase = event.eventPhase; \\cur = event.currentTarget; \\nb ++; - \\}) + \\} , .ex = "undefined" }, - .{ .src = "content.dispatchEvent(event)", .ex = "true" }, + }; + try checkCases(js_env, &common); + + var basic = [_]Case{ + .{ .src = "content.addEventListener('basic', cbk)", .ex = "undefined" }, + .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" }, .{ .src = "nb", .ex = "1" }, .{ .src = "evt instanceof Event", .ex = "true" }, - .{ .src = "evt.type", .ex = "myEvent" }, + .{ .src = "evt.type", .ex = "basic" }, .{ .src = "phase", .ex = "2" }, - .{ .src = "cur.localName", .ex = "div" }, + .{ .src = "cur.getAttribute('id')", .ex = "content" }, }; try checkCases(js_env, &basic); + + var basic_child = [_]Case{ + .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, + .{ .src = "para.dispatchEvent(new Event('basic'))", .ex = "true" }, + .{ .src = "nb", .ex = "0" }, // handler is not called, no capture, not the target, no bubbling + .{ .src = "evt === undefined", .ex = "true" }, + }; + try checkCases(js_env, &basic_child); + + var capture = [_]Case{ + .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, + .{ .src = "content.addEventListener('capture', cbk, true)", .ex = "undefined" }, + .{ .src = "content.dispatchEvent(new Event('capture'))", .ex = "true" }, + .{ .src = "nb", .ex = "1" }, + .{ .src = "evt instanceof Event", .ex = "true" }, + .{ .src = "evt.type", .ex = "capture" }, + .{ .src = "phase", .ex = "2" }, + .{ .src = "cur.getAttribute('id')", .ex = "content" }, + }; + try checkCases(js_env, &capture); + + var capture_child = [_]Case{ + .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, + .{ .src = "para.dispatchEvent(new Event('capture'))", .ex = "true" }, + .{ .src = "nb", .ex = "1" }, + .{ .src = "evt instanceof Event", .ex = "true" }, + .{ .src = "evt.type", .ex = "capture" }, + .{ .src = "phase", .ex = "1" }, + .{ .src = "cur.getAttribute('id')", .ex = "content" }, + }; + try checkCases(js_env, &capture_child); + + var bubbles = [_]Case{ + .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, + .{ .src = "content.addEventListener('bubbles', cbk)", .ex = "undefined" }, + .{ .src = "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", .ex = "true" }, + .{ .src = "nb", .ex = "1" }, + .{ .src = "evt instanceof Event", .ex = "true" }, + .{ .src = "evt.type", .ex = "bubbles" }, + .{ .src = "phase", .ex = "2" }, + .{ .src = "cur.getAttribute('id')", .ex = "content" }, + }; + try checkCases(js_env, &bubbles); + + var bubbles_child = [_]Case{ + .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, + .{ .src = "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", .ex = "true" }, + .{ .src = "nb", .ex = "1" }, + .{ .src = "evt instanceof Event", .ex = "true" }, + .{ .src = "evt.type", .ex = "bubbles" }, + .{ .src = "phase", .ex = "3" }, + .{ .src = "cur.getAttribute('id')", .ex = "content" }, + }; + try checkCases(js_env, &bubbles_child); } From de7fe8ba09c597ae2f7bafabe769e23173de7ef2 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Fri, 19 Jan 2024 16:46:09 +0100 Subject: [PATCH 05/10] Add tests for Event Signed-off-by: Francis Bouvier --- src/dom/event_target.zig | 3 ++ src/events/event.zig | 85 ++++++++++++++++++++++++++++++++++++++-- src/run_tests.zig | 2 + 3 files changed, 87 insertions(+), 3 deletions(-) diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig index 05fd3f35..b8f32581 100644 --- a/src/dom/event_target.zig +++ b/src/dom/event_target.zig @@ -60,6 +60,8 @@ pub fn testExecFn( var common = [_]Case{ .{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, .{ .src = "let para = document.getElementById('para')", .ex = "undefined" }, + // NOTE: as some event properties will change during the event dispatching phases + // we need to copy thoses values in order to check them afterwards .{ .src = \\var nb = 0; var evt; var phase; var cur; \\function cbk(event) { @@ -121,6 +123,7 @@ pub fn testExecFn( .{ .src = "nb", .ex = "1" }, .{ .src = "evt instanceof Event", .ex = "true" }, .{ .src = "evt.type", .ex = "bubbles" }, + .{ .src = "evt.bubbles", .ex = "true" }, .{ .src = "phase", .ex = "2" }, .{ .src = "cur.getAttribute('id')", .ex = "content" }, }; diff --git a/src/events/event.zig b/src/events/event.zig index 89576cbb..25dd40fe 100644 --- a/src/events/event.zig +++ b/src/events/event.zig @@ -2,6 +2,11 @@ const std = @import("std"); const generate = @import("../generate.zig"); +const jsruntime = @import("jsruntime"); +const Callback = jsruntime.Callback; +const Case = jsruntime.test_utils.Case; +const checkCases = jsruntime.test_utils.checkCases; + const parser = @import("../netsurf.zig"); const DOMException = @import("../dom/exceptions.zig").DOMException; @@ -73,15 +78,15 @@ pub const Event = struct { // Methods - pub fn get_stopPropagation(self: *parser.Event) !void { + pub fn _stopPropagation(self: *parser.Event) !void { return try parser.eventStopPropagation(self); } - pub fn get_stopImmediatePropagation(self: *parser.Event) !void { + pub fn _stopImmediatePropagation(self: *parser.Event) !void { return try parser.eventStopImmediatePropagation(self); } - pub fn get_preventDefault(self: *parser.Event) !void { + pub fn _preventDefault(self: *parser.Event) !void { return try parser.eventPreventDefault(self); } }; @@ -92,3 +97,77 @@ pub const Interfaces = generate.Tuple(.{ }); const Generated = generate.Union.compile(Interfaces); pub const Union = Generated._union; + +pub fn testExecFn( + _: std.mem.Allocator, + js_env: *jsruntime.Env, +) anyerror!void { + var common = [_]Case{ + .{ .src = "let content = document.getElementById('content')", .ex = "undefined" }, + .{ .src = "let para = document.getElementById('para')", .ex = "undefined" }, + .{ .src = "var nb = 0; var evt", .ex = "undefined" }, + }; + try checkCases(js_env, &common); + + var basic = [_]Case{ + .{ .src = + \\content.addEventListener('target', + \\function(e) { + \\evt = e; nb = nb + 1; + \\e.preventDefault(); + \\}) + , .ex = "undefined" }, + .{ .src = "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", .ex = "false" }, + .{ .src = "nb", .ex = "1" }, + .{ .src = "evt.target === content", .ex = "true" }, + .{ .src = "evt.bubbles", .ex = "true" }, + .{ .src = "evt.cancelable", .ex = "true" }, + .{ .src = "evt.defaultPrevented", .ex = "true" }, + .{ .src = "evt.isTrusted", .ex = "true" }, + .{ .src = "evt.timestamp > 1704063600", .ex = "true" }, // 2024/01/01 00:00 + // event.type, event.currentTarget, event.phase checked in EventTarget + }; + try checkCases(js_env, &basic); + + var stop = [_]Case{ + .{ .src = "nb = 0", .ex = "0" }, + .{ .src = + \\content.addEventListener('stop', + \\function(e) { + \\e.stopPropagation(); + \\nb = nb + 1; + \\}, true) + , .ex = "undefined" }, + // the following event listener will not be invoked + .{ .src = + \\para.addEventListener('stop', + \\function(e) { + \\nb = nb + 1; + \\}) + , .ex = "undefined" }, + .{ .src = "para.dispatchEvent(new Event('stop'))", .ex = "true" }, + .{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at content event listener + }; + try checkCases(js_env, &stop); + + var stop_immediate = [_]Case{ + .{ .src = "nb = 0", .ex = "0" }, + .{ .src = + \\content.addEventListener('immediate', + \\function(e) { + \\e.stopImmediatePropagation(); + \\nb = nb + 1; + \\}) + , .ex = "undefined" }, + // the following event listener will not be invoked + .{ .src = + \\content.addEventListener('immediate', + \\function(e) { + \\nb = nb + 1; + \\}) + , .ex = "undefined" }, + .{ .src = "content.dispatchEvent(new Event('immediate'))", .ex = "true" }, + .{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at first content event listener + }; + try checkCases(js_env, &stop_immediate); +} diff --git a/src/run_tests.zig b/src/run_tests.zig index f54f5186..22adffe9 100644 --- a/src/run_tests.zig +++ b/src/run_tests.zig @@ -21,6 +21,7 @@ const DOMTokenListExecFn = @import("dom/token_list.zig").testExecFn; const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn; const AttrTestExecFn = @import("dom/attribute.zig").testExecFn; const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn; +const EventTestExecFn = @import("events/event.zig").testExecFn; pub const Types = jsruntime.reflect(apiweb.Interfaces); @@ -75,6 +76,7 @@ fn testsAllExecFn( NodeListTestExecFn, AttrTestExecFn, EventTargetTestExecFn, + EventTestExecFn, }; inline for (testFns) |testFn| { From e7dcf426f3273419295deaf009a0b5cd9fffae86 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Sat, 20 Jan 2024 17:47:29 +0100 Subject: [PATCH 06/10] Update WPT with events files Signed-off-by: Francis Bouvier --- tests/wpt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/wpt b/tests/wpt index c9e76582..a2c7f5a2 160000 --- a/tests/wpt +++ b/tests/wpt @@ -1 +1 @@ -Subproject commit c9e7658223455f1e3ce11d348cb79528326c7c4c +Subproject commit a2c7f5a24dd5269e8475fc3cae18c4719058dedc From 3483dfa81e07b73881e18d52c4047181af636e55 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Tue, 23 Jan 2024 02:56:12 +0100 Subject: [PATCH 07/10] Check if event target has listener before adding it Signed-off-by: Francis Bouvier --- src/dom/event_target.zig | 15 +++++++++++++ src/netsurf.zig | 47 +++++++++++++++++++++++++++++++++++++++- vendor/jsruntime-lib | 2 +- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig index b8f32581..0f8fd4b4 100644 --- a/src/dom/event_target.zig +++ b/src/dom/event_target.zig @@ -37,6 +37,13 @@ pub const EventTarget = struct { // 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, eventType, cbk.id()); + if (lst != null) { + return; + } + // TODO: when can we free this allocation? const cbk_ptr = try alloc.create(Callback); cbk_ptr.* = cbk; @@ -85,6 +92,14 @@ pub fn testExecFn( }; try checkCases(js_env, &basic); + var basic_twice = [_]Case{ + .{ .src = "nb = 0", .ex = "0" }, + .{ .src = "content.addEventListener('basic', cbk)", .ex = "undefined" }, + .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" }, + .{ .src = "nb", .ex = "1" }, + }; + try checkCases(js_env, &basic_twice); + var basic_child = [_]Case{ .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, .{ .src = "para.dispatchEvent(new Event('basic'))", .ex = "true" }, diff --git a/src/netsurf.zig b/src/netsurf.zig index b609768b..ddf0e833 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -458,6 +458,13 @@ const event_handler = struct { } }.handle; +// EventListener +pub const EventListener = c.dom_event_listener; + +pub fn eventListenerGetData(lst: *EventListener) ?*anyopaque { + return c.dom_event_listener_get_data(lst); +} + // EventTarget pub const EventTarget = c.dom_event_target; @@ -465,6 +472,44 @@ fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable { return getVtable(c.dom_event_target_vtable, EventTarget, et); } +pub fn eventTargetHasListener(et: *EventTarget, typ: []const u8, cbk_id: usize) !?*EventListener { + const str = try strFromData(typ); + + const EventListenerEntry = c.listener_entry; + var current: ?*EventListenerEntry = null; + var next: ?*EventListenerEntry = undefined; + var lst: ?*EventListener = undefined; + + // iterate over the EventTarget's listeners + while (true) { + const err = eventTargetVtable(et).iter_event_listener.?(et, str, current, &next, &lst); + try DOMErr(err); + + if (lst) |listener| { + // the EventTarget has a listener for this event type, + // let's check if the callback is the same + defer c.dom_event_listener_unref(listener); + const data = eventListenerGetData(listener); + if (data) |d| { + const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(d); + const cbk = @as(*Callback, @ptrCast(ptr)); + if (cbk_id == cbk.id()) + return lst; + } + } + + if (next == null) { + // no more listeners, end of the iteration + break; + } + + // next iteration + current = next; + } + + return null; +} + pub fn eventTargetAddEventListener( et: *EventTarget, typ: []const u8, @@ -473,7 +518,7 @@ pub fn eventTargetAddEventListener( ) !void { const s = try strFromData(typ); const ctx = @as(*anyopaque, @ptrCast(cbk_ptr)); - var listener: ?*c.dom_event_listener = undefined; + var listener: ?*EventListener = undefined; const errLst = c.dom_event_listener_create(event_handler, ctx, &listener); try DOMErr(errLst); const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture); diff --git a/vendor/jsruntime-lib b/vendor/jsruntime-lib index 7de65ccd..637cd3a3 160000 --- a/vendor/jsruntime-lib +++ b/vendor/jsruntime-lib @@ -1 +1 @@ -Subproject commit 7de65ccd304b25a8db69c57787bf28cd3b188b90 +Subproject commit 637cd3a34471793767a15da2f9898900561bc117 From 9ba4c69a92eab0a3683726c306b0ea82f2dff75b Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Wed, 24 Jan 2024 11:43:16 +0100 Subject: [PATCH 08/10] Add removeEventListener And check if callback has been already added in addEventListener Signed-off-by: Francis Bouvier --- src/dom/event_target.zig | 94 +++++++++++++++++++++++++++++++++++----- src/netsurf.zig | 57 +++++++++++++++++++----- 2 files changed, 129 insertions(+), 22 deletions(-) diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig index 0f8fd4b4..22428b46 100644 --- a/src/dom/event_target.zig +++ b/src/dom/event_target.zig @@ -2,6 +2,7 @@ const std = @import("std"); const jsruntime = @import("jsruntime"); const Callback = jsruntime.Callback; +const JSObjectID = jsruntime.JSObjectID; const Case = jsruntime.test_utils.Case; const checkCases = jsruntime.test_utils.checkCases; @@ -39,22 +40,71 @@ pub const EventTarget = struct { ) !void { // check if event target has already this listener - const lst = try parser.eventTargetHasListener(self, eventType, cbk.id()); + const lst = try parser.eventTargetHasListener( + self, + eventType, + capture orelse false, + cbk.id(), + ); if (lst != null) { return; } - // TODO: when can we free this allocation? + // NOTE: this allocation will be removed either if removeEventListener + // or at EventTarget deinit const cbk_ptr = try alloc.create(Callback); cbk_ptr.* = cbk; - try parser.eventTargetAddEventListener(self, eventType, cbk_ptr, capture orelse false); + try parser.eventTargetAddEventListener( + self, + eventType, + cbk_ptr, + capture orelse false, + ); + } + + pub fn _removeEventListener( + self: *parser.EventTarget, + alloc: std.mem.Allocator, + eventType: []const u8, + cbk_id: JSObjectID, + 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, + eventType, + capture orelse false, + cbk_id.get(), + ); + if (lst == null) { + return; + } + + // remove listener + const cbk_handler = try parser.eventTargetRemoveEventListener( + self, + eventType, + lst.?, + capture orelse false, + ); + if (cbk_handler) |cbk_ptr| { + cbk_ptr.deinit(alloc); + alloc.destroy(cbk_ptr); + } } pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool { return try parser.eventTargetDispatchEvent(self, event); } - pub fn deinit(_: *parser.EventTarget, _: std.mem.Allocator) void {} + pub fn deinit(_: *parser.EventTarget, _: std.mem.Allocator) void { + // TODO: + // - deinit and destroy all cbk_handler + // - remove all listeners + } }; // Tests @@ -92,6 +142,14 @@ pub fn testExecFn( }; try checkCases(js_env, &basic); + var basic_child = [_]Case{ + .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, + .{ .src = "para.dispatchEvent(new Event('basic'))", .ex = "true" }, + .{ .src = "nb", .ex = "0" }, // handler is not called, no capture, not the target, no bubbling + .{ .src = "evt === undefined", .ex = "true" }, + }; + try checkCases(js_env, &basic_child); + var basic_twice = [_]Case{ .{ .src = "nb = 0", .ex = "0" }, .{ .src = "content.addEventListener('basic', cbk)", .ex = "undefined" }, @@ -100,13 +158,29 @@ pub fn testExecFn( }; try checkCases(js_env, &basic_twice); - var basic_child = [_]Case{ - .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, - .{ .src = "para.dispatchEvent(new Event('basic'))", .ex = "true" }, - .{ .src = "nb", .ex = "0" }, // handler is not called, no capture, not the target, no bubbling - .{ .src = "evt === undefined", .ex = "true" }, + var basic_twice_capture = [_]Case{ + .{ .src = "nb = 0", .ex = "0" }, + .{ .src = "content.addEventListener('basic', cbk, true)", .ex = "undefined" }, + .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" }, + .{ .src = "nb", .ex = "2" }, }; - try checkCases(js_env, &basic_child); + try checkCases(js_env, &basic_twice_capture); + + var basic_remove = [_]Case{ + .{ .src = "nb = 0", .ex = "0" }, + .{ .src = "content.removeEventListener('basic', cbk)", .ex = "undefined" }, + .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" }, + .{ .src = "nb", .ex = "1" }, + }; + try checkCases(js_env, &basic_remove); + + var basic_capture_remove = [_]Case{ + .{ .src = "nb = 0", .ex = "0" }, + .{ .src = "content.removeEventListener('basic', cbk, true)", .ex = "undefined" }, + .{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" }, + .{ .src = "nb", .ex = "0" }, + }; + try checkCases(js_env, &basic_capture_remove); var capture = [_]Case{ .{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" }, diff --git a/src/netsurf.zig b/src/netsurf.zig index ddf0e833..69cbabd5 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -446,15 +446,21 @@ pub fn eventPreventDefault(evt: *Event) !void { // EventHandler pub const EventHandler = fn (?*Event, ?*anyopaque) callconv(.C) void; +fn event_handler_cbk(data: *anyopaque) *Callback { + const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data); + return @as(*Callback, @ptrCast(ptr)); +} + const event_handler = struct { fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void { - const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data.?); - const func = @as(*Callback, @ptrCast(ptr)); - func.call(.{event}) catch unreachable; - // NOTE: we can not call func.deinit here - // b/c the handler can be called several times - // as the event goes through the ancestors - // TODO: check the event phase to call func.deinit and free func + if (data) |d| { + const func = event_handler_cbk(d); + func.call(.{event}) catch unreachable; + // NOTE: we can not call func.deinit here + // b/c the handler can be called several times + // as the event goes through the ancestors + // TODO: check the event phase to call func.deinit and free func + } } }.handle; @@ -472,7 +478,12 @@ fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable { return getVtable(c.dom_event_target_vtable, EventTarget, et); } -pub fn eventTargetHasListener(et: *EventTarget, typ: []const u8, cbk_id: usize) !?*EventListener { +pub fn eventTargetHasListener( + et: *EventTarget, + typ: []const u8, + capture: bool, + cbk_id: usize, +) !?*EventListener { const str = try strFromData(typ); const EventListenerEntry = c.listener_entry; @@ -482,7 +493,14 @@ pub fn eventTargetHasListener(et: *EventTarget, typ: []const u8, cbk_id: usize) // iterate over the EventTarget's listeners while (true) { - const err = eventTargetVtable(et).iter_event_listener.?(et, str, current, &next, &lst); + const err = eventTargetVtable(et).iter_event_listener.?( + et, + str, + capture, + current, + &next, + &lst, + ); try DOMErr(err); if (lst) |listener| { @@ -491,8 +509,7 @@ pub fn eventTargetHasListener(et: *EventTarget, typ: []const u8, cbk_id: usize) defer c.dom_event_listener_unref(listener); const data = eventListenerGetData(listener); if (data) |d| { - const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(d); - const cbk = @as(*Callback, @ptrCast(ptr)); + const cbk = event_handler_cbk(d); if (cbk_id == cbk.id()) return lst; } @@ -525,9 +542,25 @@ pub fn eventTargetAddEventListener( try DOMErr(err); } +pub fn eventTargetRemoveEventListener( + et: *EventTarget, + typ: []const u8, + lst: *EventListener, + capture: bool, +) !?*Callback { + const data = eventListenerGetData(lst); + var cbk_ptr: ?*Callback = null; + if (data) |d| { + cbk_ptr = event_handler_cbk(d); + } + const s = try strFromData(typ); + const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture); + try DOMErr(err); + return cbk_ptr; +} + pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool { var res: bool = undefined; - // const err = c.dom_event_target_dispatch_event(et, event, &res); const err = eventTargetVtable(et).dispatch_event.?(et, event, &res); try DOMErr(err); return res; From ce3c5336802d08e47e92e19855dbfa19ff845ace Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Mon, 29 Jan 2024 08:01:35 +0100 Subject: [PATCH 09/10] Add deinit for EventTarget (eventTargetRemoveAllEventListeners) And unify the way allocator is used on eventTarget add/remove listener Signed-off-by: Francis Bouvier --- src/dom/event_target.zig | 20 +++------- src/netsurf.zig | 86 +++++++++++++++++++++++++++++++++------- vendor/jsruntime-lib | 2 +- 3 files changed, 78 insertions(+), 30 deletions(-) diff --git a/src/dom/event_target.zig b/src/dom/event_target.zig index 22428b46..84e53b40 100644 --- a/src/dom/event_target.zig +++ b/src/dom/event_target.zig @@ -50,14 +50,11 @@ pub const EventTarget = struct { return; } - // NOTE: this allocation will be removed either if removeEventListener - // or at EventTarget deinit - const cbk_ptr = try alloc.create(Callback); - cbk_ptr.* = cbk; try parser.eventTargetAddEventListener( self, + alloc, eventType, - cbk_ptr, + cbk, capture orelse false, ); } @@ -84,26 +81,21 @@ pub const EventTarget = struct { } // remove listener - const cbk_handler = try parser.eventTargetRemoveEventListener( + try parser.eventTargetRemoveEventListener( self, + alloc, eventType, lst.?, capture orelse false, ); - if (cbk_handler) |cbk_ptr| { - cbk_ptr.deinit(alloc); - alloc.destroy(cbk_ptr); - } } pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool { return try parser.eventTargetDispatchEvent(self, event); } - pub fn deinit(_: *parser.EventTarget, _: std.mem.Allocator) void { - // TODO: - // - deinit and destroy all cbk_handler - // - remove all listeners + pub fn deinit(self: *parser.EventTarget, alloc: std.mem.Allocator) void { + parser.eventTargetRemoveAllEventListeners(self, alloc) catch unreachable; } }; diff --git a/src/netsurf.zig b/src/netsurf.zig index 69cbabd5..7b40363c 100644 --- a/src/netsurf.zig +++ b/src/netsurf.zig @@ -444,8 +444,6 @@ pub fn eventPreventDefault(evt: *Event) !void { } // EventHandler -pub const EventHandler = fn (?*Event, ?*anyopaque) callconv(.C) void; - fn event_handler_cbk(data: *anyopaque) *Callback { const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data); return @as(*Callback, @ptrCast(ptr)); @@ -458,16 +456,16 @@ const event_handler = struct { func.call(.{event}) catch unreachable; // NOTE: we can not call func.deinit here // b/c the handler can be called several times - // as the event goes through the ancestors - // TODO: check the event phase to call func.deinit and free func + // either on this dispatch event or in anoter one } } }.handle; // EventListener pub const EventListener = c.dom_event_listener; +const EventListenerEntry = c.listener_entry; -pub fn eventListenerGetData(lst: *EventListener) ?*anyopaque { +fn eventListenerGetData(lst: *EventListener) ?*anyopaque { return c.dom_event_listener_get_data(lst); } @@ -486,7 +484,6 @@ pub fn eventTargetHasListener( ) !?*EventListener { const str = try strFromData(typ); - const EventListenerEntry = c.listener_entry; var current: ?*EventListenerEntry = null; var next: ?*EventListenerEntry = undefined; var lst: ?*EventListener = undefined; @@ -504,14 +501,16 @@ pub fn eventTargetHasListener( try DOMErr(err); if (lst) |listener| { - // the EventTarget has a listener for this event type, - // let's check if the callback is the same + // the EventTarget has a listener for this event type + // and capture property, + // let's check if the callback handler is the same defer c.dom_event_listener_unref(listener); const data = eventListenerGetData(listener); if (data) |d| { const cbk = event_handler_cbk(d); - if (cbk_id == cbk.id()) + if (cbk_id == cbk.id()) { return lst; + } } } @@ -529,34 +528,91 @@ pub fn eventTargetHasListener( pub fn eventTargetAddEventListener( et: *EventTarget, + alloc: std.mem.Allocator, typ: []const u8, - cbk_ptr: *Callback, + cbk: Callback, capture: bool, ) !void { - const s = try strFromData(typ); + // this allocation will be removed either on + // eventTargetRemoveEventListener or eventTargetRemoveAllEventListeners + const cbk_ptr = try alloc.create(Callback); + cbk_ptr.* = cbk; + const ctx = @as(*anyopaque, @ptrCast(cbk_ptr)); var listener: ?*EventListener = undefined; const errLst = c.dom_event_listener_create(event_handler, ctx, &listener); try DOMErr(errLst); + defer c.dom_event_listener_unref(listener); + + const s = try strFromData(typ); const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture); try DOMErr(err); } pub fn eventTargetRemoveEventListener( et: *EventTarget, + alloc: std.mem.Allocator, typ: []const u8, lst: *EventListener, capture: bool, -) !?*Callback { +) !void { const data = eventListenerGetData(lst); - var cbk_ptr: ?*Callback = null; + // free cbk allocation made on eventTargetAddEventListener if (data) |d| { - cbk_ptr = event_handler_cbk(d); + const cbk_ptr = event_handler_cbk(d); + cbk_ptr.deinit(alloc); + alloc.destroy(cbk_ptr); } + const s = try strFromData(typ); const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture); try DOMErr(err); - return cbk_ptr; +} + +pub fn eventTargetRemoveAllEventListeners( + et: *EventTarget, + alloc: std.mem.Allocator, +) !void { + var next: ?*EventListenerEntry = undefined; + var lst: ?*EventListener = undefined; + + // iterate over the EventTarget's listeners + while (true) { + const errIter = eventTargetVtable(et).iter_event_listener.?( + et, + null, + false, + null, + &next, + &lst, + ); + try DOMErr(errIter); + + if (lst) |listener| { + defer c.dom_event_listener_unref(listener); + const data = eventListenerGetData(listener); + if (data) |d| { + // free cbk allocation made on eventTargetAddEventListener + const cbk = event_handler_cbk(d); + cbk.deinit(alloc); + alloc.destroy(cbk); + } + const err = eventTargetVtable(et).remove_event_listener.?( + et, + null, + lst, + false, + ); + try DOMErr(err); + } + + if (next == null) { + // no more listeners, end of the iteration + break; + } + + // next iteration + } } pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool { diff --git a/vendor/jsruntime-lib b/vendor/jsruntime-lib index 637cd3a3..2af9e9b7 160000 --- a/vendor/jsruntime-lib +++ b/vendor/jsruntime-lib @@ -1 +1 @@ -Subproject commit 637cd3a34471793767a15da2f9898900561bc117 +Subproject commit 2af9e9b7fc0ac94918d32a44822f050f939e47b6 From 501b3caa8ea2300db4fa5b390d88cd313a53cfe8 Mon Sep 17 00:00:00 2001 From: Francis Bouvier Date: Mon, 29 Jan 2024 09:04:03 +0100 Subject: [PATCH 10/10] Add legacy document.createEvent and event.initEvent Signed-off-by: Francis Bouvier --- src/dom/document.zig | 9 +++++++++ src/events/event.zig | 28 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+) diff --git a/src/dom/document.zig b/src/dom/document.zig index 702d92e2..7f3af5cf 100644 --- a/src/dom/document.zig +++ b/src/dom/document.zig @@ -81,6 +81,15 @@ pub const Document = struct { return try parser.documentGetDoctype(self); } + pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event { + // TODO: for now only "Event" constructor is supported + // see table on https://dom.spec.whatwg.org/#dom-document-createevent $2 + if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) { + return try parser.eventCreate(); + } + return parser.DOMError.NotSupported; + } + pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion { const e = try parser.documentGetElementById(self, id) orelse return null; return try Element.toInterface(e); diff --git a/src/events/event.zig b/src/events/event.zig index 25dd40fe..fe939621 100644 --- a/src/events/event.zig +++ b/src/events/event.zig @@ -78,6 +78,19 @@ pub const Event = struct { // Methods + pub fn _initEvent( + self: *parser.Event, + eventType: []const u8, + bubbles: ?bool, + cancelable: ?bool, + ) !void { + const opts = EventInit{ + .bubbles = bubbles orelse false, + .cancelable = cancelable orelse false, + }; + return try parser.eventInit(self, eventType, opts); + } + pub fn _stopPropagation(self: *parser.Event) !void { return try parser.eventStopPropagation(self); } @@ -170,4 +183,19 @@ pub fn testExecFn( .{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at first content event listener }; try checkCases(js_env, &stop_immediate); + + var legacy = [_]Case{ + .{ .src = "nb = 0", .ex = "0" }, + .{ .src = + \\content.addEventListener('legacy', + \\function(e) { + \\evt = e; nb = nb + 1; + \\}) + , .ex = "undefined" }, + .{ .src = "let evtLegacy = document.createEvent('Event')", .ex = "undefined" }, + .{ .src = "evtLegacy.initEvent('legacy')", .ex = "undefined" }, + .{ .src = "content.dispatchEvent(evtLegacy)", .ex = "true" }, + .{ .src = "nb", .ex = "1" }, + }; + try checkCases(js_env, &legacy); }