diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 7acb8473..bc380d1a 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -544,6 +544,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/encoding/TextDecoder.zig"),
@import("../webapi/encoding/TextEncoder.zig"),
@import("../webapi/Event.zig"),
+ @import("../webapi/event/CustomEvent.zig"),
@import("../webapi/event/ErrorEvent.zig"),
@import("../webapi/event/ProgressEvent.zig"),
@import("../webapi/EventTarget.zig"),
diff --git a/src/browser/tests/event/custom_event.html b/src/browser/tests/event/custom_event.html
new file mode 100644
index 00000000..97f114d8
--- /dev/null
+++ b/src/browser/tests/event/custom_event.html
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig
index eaa9b6e0..e895bfd4 100644
--- a/src/browser/webapi/Document.zig
+++ b/src/browser/webapi/Document.zig
@@ -175,6 +175,26 @@ pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node
return page.createTextNode(data);
}
+pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@import("Event.zig") {
+ const Event = @import("Event.zig");
+
+ if (std.ascii.eqlIgnoreCase(event_type, "event") or std.ascii.eqlIgnoreCase(event_type, "events") or std.ascii.eqlIgnoreCase(event_type, "htmlevents")) {
+ return Event.init("", null, page);
+ }
+
+ if (std.ascii.eqlIgnoreCase(event_type, "customevent") or std.ascii.eqlIgnoreCase(event_type, "customevents")) {
+ const CustomEvent = @import("event/CustomEvent.zig");
+ const custom_event = try CustomEvent.init("", null, page);
+ return custom_event.asEvent();
+ }
+
+ if (std.ascii.eqlIgnoreCase(event_type, "messageevent")) {
+ return error.NotSupported;
+ }
+
+ return error.NotSupported;
+}
+
pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?u32, filter: ?DOMTreeWalker.FilterOpts, page: *Page) !*DOMTreeWalker {
const show = what_to_show orelse NodeFilter.SHOW_ALL;
return DOMTreeWalker.init(root, show, filter, page);
@@ -239,6 +259,7 @@ pub const JsApi = struct {
pub const createDocumentFragment = bridge.function(Document.createDocumentFragment, .{});
pub const createComment = bridge.function(Document.createComment, .{});
pub const createTextNode = bridge.function(Document.createTextNode, .{});
+ pub const createEvent = bridge.function(Document.createEvent, .{ .dom_exception = true });
pub const createTreeWalker = bridge.function(Document.createTreeWalker, .{});
pub const createNodeIterator = bridge.function(Document.createNodeIterator, .{});
pub const getElementById = bridge.function(Document.getElementById, .{});
diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig
index 9884ff85..70de6e07 100644
--- a/src/browser/webapi/Event.zig
+++ b/src/browser/webapi/Event.zig
@@ -48,6 +48,7 @@ pub const Type = union(enum) {
generic,
progress_event: *@import("event/ProgressEvent.zig"),
error_event: *@import("event/ErrorEvent.zig"),
+ custom_event: *@import("event/CustomEvent.zig"),
};
const Options = struct {
diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig
new file mode 100644
index 00000000..1c36fc33
--- /dev/null
+++ b/src/browser/webapi/event/CustomEvent.zig
@@ -0,0 +1,78 @@
+// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+//
+// Francis Bouvier
+// Pierre Tachoire
+//
+// 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 .
+
+const std = @import("std");
+const js = @import("../../js/js.zig");
+
+const Page = @import("../../Page.zig");
+const Event = @import("../Event.zig");
+const Allocator = std.mem.Allocator;
+
+const CustomEvent = @This();
+
+_proto: *Event,
+_detail: ?js.Object = null,
+_arena: Allocator,
+
+pub const InitOptions = struct {
+ detail: ?js.Object = null,
+ bubbles: bool = false,
+ cancelable: bool = false,
+};
+
+pub fn init(typ: []const u8, opts_: ?InitOptions, page: *Page) !*CustomEvent {
+ const arena = page.arena;
+ const opts = opts_ orelse InitOptions{};
+
+ const event = try page._factory.event(typ, CustomEvent{
+ ._arena = arena,
+ ._proto = undefined,
+ ._detail = if (opts.detail) |detail| try detail.persist() else null,
+ });
+
+ event._proto._bubbles = opts.bubbles;
+ event._proto._cancelable = opts.cancelable;
+
+ return event;
+}
+
+pub fn asEvent(self: *CustomEvent) *Event {
+ return self._proto;
+}
+
+pub fn getDetail(self: *const CustomEvent) ?js.Object {
+ return self._detail;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(CustomEvent);
+
+ pub const Meta = struct {
+ pub const name = "CustomEvent";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const constructor = bridge.constructor(CustomEvent.init, .{});
+ pub const detail = bridge.accessor(CustomEvent.getDetail, null, .{});
+};
+
+const testing = @import("../../../testing.zig");
+test "WebApi: CustomEvent" {
+ try testing.htmlRunner("event/custom_event.html", .{});
+}