Files
browser/src/dom/event_target.zig
Pierre Tachoire 1924f136c6 events: create an EventHandlerData struct
It simplifies the EventHandlerFunc creation and allows to insert user's
data.
2024-06-21 09:20:03 +02:00

244 lines
8.8 KiB
Zig

// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// 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 jsruntime = @import("jsruntime");
const Callback = jsruntime.Callback;
const JSObjectID = jsruntime.JSObjectID;
const Case = jsruntime.test_utils.Case;
const checkCases = jsruntime.test_utils.checkCases;
const parser = @import("netsurf");
const EventHandler = @import("../events/event.zig").EventHandler;
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
// --------
pub fn _addEventListener(
self: *parser.EventTarget,
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 {
// check if event target has already this listener
const lst = try parser.eventTargetHasListener(
self,
eventType,
capture orelse false,
cbk.id(),
);
if (lst != null) {
return;
}
try parser.eventTargetAddEventListener(
self,
alloc,
eventType,
EventHandler,
.{ .cbk = cbk },
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
try parser.eventTargetRemoveEventListener(
self,
alloc,
eventType,
lst.?,
capture orelse false,
);
}
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
return try parser.eventTargetDispatchEvent(self, event);
}
pub fn deinit(self: *parser.EventTarget, alloc: std.mem.Allocator) void {
parser.eventTargetRemoveAllEventListeners(self, alloc) catch unreachable;
}
};
// Tests
// -----
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" },
// 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) {
\\evt = event;
\\phase = event.eventPhase;
\\cur = event.currentTarget;
\\nb ++;
\\}
, .ex = "undefined" },
};
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 = "basic" },
.{ .src = "phase", .ex = "2" },
.{ .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 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_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_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" },
.{ .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 = "evt.bubbles", .ex = "true" },
.{ .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);
}