diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig
index 4d7ad032..72cb14a6 100644
--- a/src/browser/js/bridge.zig
+++ b/src/browser/js/bridge.zig
@@ -926,6 +926,7 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/event/KeyboardEvent.zig"),
@import("../webapi/event/FocusEvent.zig"),
@import("../webapi/event/WheelEvent.zig"),
+ @import("../webapi/event/DragEvent.zig"),
@import("../webapi/event/TextEvent.zig"),
@import("../webapi/event/InputEvent.zig"),
@import("../webapi/event/PromiseRejectionEvent.zig"),
diff --git a/src/browser/tests/event/drag.html b/src/browser/tests/event/drag.html
new file mode 100644
index 00000000..db3ddaa0
--- /dev/null
+++ b/src/browser/tests/event/drag.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/browser/webapi/event/DragEvent.zig b/src/browser/webapi/event/DragEvent.zig
new file mode 100644
index 00000000..fe0def98
--- /dev/null
+++ b/src/browser/webapi/event/DragEvent.zig
@@ -0,0 +1,134 @@
+// Copyright (C) 2023-2026 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 lp = @import("lightpanda");
+
+const js = @import("../../js/js.zig");
+const Frame = @import("../../Frame.zig");
+const Page = @import("../../Page.zig");
+
+const Event = @import("../Event.zig");
+const MouseEvent = @import("MouseEvent.zig");
+const DataTransfer = @import("../DataTransfer.zig");
+
+const String = lp.String;
+
+const DragEvent = @This();
+
+_proto: *MouseEvent,
+_data_transfer: ?*DataTransfer,
+
+pub const DragEventOptions = struct {
+ dataTransfer: ?*DataTransfer = null,
+};
+
+pub const Options = Event.inheritOptions(DragEvent, DragEventOptions);
+
+pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*DragEvent {
+ return initWithTrusted(typ, _opts, false, frame);
+}
+
+pub fn initTrusted(typ: []const u8, _opts: ?Options, frame: *Frame) !*DragEvent {
+ return initWithTrusted(typ, _opts, true, frame);
+}
+
+fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, frame: *Frame) !*DragEvent {
+ const arena = try frame.getArena(.medium, "DragEvent");
+ errdefer frame.releaseArena(arena);
+ const type_string = try String.init(arena, typ, .{});
+
+ const opts = _opts orelse Options{};
+
+ const event = try frame._factory.mouseEvent(
+ arena,
+ type_string,
+ MouseEvent{
+ ._type = .{ .drag_event = undefined },
+ ._proto = undefined,
+ ._screen_x = opts.screenX,
+ ._screen_y = opts.screenY,
+ ._client_x = opts.clientX,
+ ._client_y = opts.clientY,
+ ._ctrl_key = opts.ctrlKey,
+ ._shift_key = opts.shiftKey,
+ ._alt_key = opts.altKey,
+ ._meta_key = opts.metaKey,
+ ._button = std.meta.intToEnum(MouseEvent.MouseButton, opts.button) catch return error.TypeError,
+ ._buttons = opts.buttons,
+ ._related_target = opts.relatedTarget,
+ },
+ DragEvent{
+ ._proto = undefined,
+ ._data_transfer = opts.dataTransfer,
+ },
+ );
+
+ Event.populatePrototypes(event, opts, trusted);
+
+ // Hold a ref on the DataTransfer so its arena outlives this event even if the
+ // JS wrapper is collected first; released in deinit (mirrors MessageEvent's
+ // Blob handling). The shared refcount lives on the Event base, so reach it
+ // through asEvent() rather than the immediate _proto.
+ if (opts.dataTransfer) |dt| {
+ dt.acquireRef();
+ }
+
+ return event;
+}
+
+pub fn deinit(self: *DragEvent, page: *Page) void {
+ if (self._data_transfer) |dt| {
+ dt.releaseRef(page);
+ }
+ self.asEvent().deinit(page);
+}
+
+pub fn acquireRef(self: *DragEvent) void {
+ self.asEvent().acquireRef();
+}
+
+pub fn releaseRef(self: *DragEvent, page: *Page) void {
+ self.asEvent()._rc.release(self, page);
+}
+
+pub fn asEvent(self: *DragEvent) *Event {
+ return self._proto.asEvent();
+}
+
+pub fn getDataTransfer(self: *const DragEvent) ?*DataTransfer {
+ return self._data_transfer;
+}
+
+pub const JsApi = struct {
+ pub const bridge = js.Bridge(DragEvent);
+
+ pub const Meta = struct {
+ pub const name = "DragEvent";
+ pub const prototype_chain = bridge.prototypeChain();
+ pub var class_id: bridge.ClassId = undefined;
+ };
+
+ pub const constructor = bridge.constructor(DragEvent.init, .{});
+ pub const dataTransfer = bridge.accessor(DragEvent.getDataTransfer, null, .{});
+};
+
+const testing = @import("../../../testing.zig");
+test "WebApi: DragEvent" {
+ try testing.htmlRunner("event/drag.html", .{});
+}
diff --git a/src/browser/webapi/event/InputEvent.zig b/src/browser/webapi/event/InputEvent.zig
index c74e8e03..f942c161 100644
--- a/src/browser/webapi/event/InputEvent.zig
+++ b/src/browser/webapi/event/InputEvent.zig
@@ -21,9 +21,11 @@ const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
+const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const UIEvent = @import("UIEvent.zig");
+const DataTransfer = @import("../DataTransfer.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
@@ -32,12 +34,13 @@ const InputEvent = @This();
_proto: *UIEvent,
_data: ?[]const u8,
-// TODO: add dataTransfer
+_data_transfer: ?*DataTransfer = null,
_input_type: []const u8,
_is_composing: bool,
pub const InputEventOptions = struct {
data: ?[]const u8 = null,
+ dataTransfer: ?*DataTransfer = null,
inputType: ?[]const u8 = null,
isComposing: bool = false,
};
@@ -69,6 +72,7 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
InputEvent{
._proto = undefined,
._data = if (opts.data) |d| try arena.dupe(u8, d) else null,
+ ._data_transfer = opts.dataTransfer,
._input_type = if (opts.inputType) |it| try arena.dupe(u8, it) else "",
._is_composing = opts.isComposing,
},
@@ -82,9 +86,31 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
rootevt._cancelable = false;
rootevt._composed = true;
+ // Hold a ref on the DataTransfer (when present) for this event's lifetime;
+ // released in deinit. Almost always null for input events, but keeps the
+ // refcount protocol intact when it isn't.
+ if (opts.dataTransfer) |dt| {
+ dt.acquireRef();
+ }
+
return event;
}
+pub fn deinit(self: *InputEvent, page: *Page) void {
+ if (self._data_transfer) |dt| {
+ dt.releaseRef(page);
+ }
+ self.asEvent().deinit(page);
+}
+
+pub fn acquireRef(self: *InputEvent) void {
+ self.asEvent().acquireRef();
+}
+
+pub fn releaseRef(self: *InputEvent, page: *Page) void {
+ self.asEvent()._rc.release(self, page);
+}
+
pub fn asEvent(self: *InputEvent) *Event {
return self._proto.asEvent();
}
@@ -93,6 +119,10 @@ pub fn getData(self: *const InputEvent) ?[]const u8 {
return self._data;
}
+pub fn getDataTransfer(self: *const InputEvent) ?*DataTransfer {
+ return self._data_transfer;
+}
+
pub fn getInputType(self: *const InputEvent) []const u8 {
return self._input_type;
}
@@ -112,6 +142,7 @@ pub const JsApi = struct {
pub const constructor = bridge.constructor(InputEvent.init, .{});
pub const data = bridge.accessor(InputEvent.getData, null, .{});
+ pub const dataTransfer = bridge.accessor(InputEvent.getDataTransfer, null, .{});
pub const inputType = bridge.accessor(InputEvent.getInputType, null, .{});
pub const isComposing = bridge.accessor(InputEvent.getIsComposing, null, .{});
};
diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig
index 240ce6c8..04a07ffe 100644
--- a/src/browser/webapi/event/MouseEvent.zig
+++ b/src/browser/webapi/event/MouseEvent.zig
@@ -1,4 +1,4 @@
-// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
+// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier
// Pierre Tachoire
@@ -45,6 +45,7 @@ pub const Type = union(enum) {
generic,
pointer_event: *PointerEvent,
wheel_event: *@import("WheelEvent.zig"),
+ drag_event: *@import("DragEvent.zig"),
};
_type: Type,
@@ -135,6 +136,7 @@ pub fn is(self: *MouseEvent, comptime T: type) ?*T {
.generic => return if (T == MouseEvent) self else null,
.pointer_event => |e| return if (T == PointerEvent) e else null,
.wheel_event => |e| return if (T == @import("WheelEvent.zig")) e else null,
+ .drag_event => |e| return if (T == @import("DragEvent.zig")) e else null,
}
return null;
}