diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index cf6e999e..4fc96b7e 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -848,6 +848,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/event/FocusEvent.zig"), @import("../webapi/event/WheelEvent.zig"), @import("../webapi/event/TextEvent.zig"), + @import("../webapi/event/InputEvent.zig"), @import("../webapi/event/PromiseRejectionEvent.zig"), @import("../webapi/MessageChannel.zig"), @import("../webapi/MessagePort.zig"), diff --git a/src/browser/tests/element/html/input.html b/src/browser/tests/element/html/input.html index 79cd0c6d..2b8576b5 100644 --- a/src/browser/tests/element/html/input.html +++ b/src/browser/tests/element/html/input.html @@ -191,14 +191,14 @@ let eventCount = 0; let lastEvent = null; - + input.addEventListener('selectionchange', (e) => { eventCount++; lastEvent = e; }); - + testing.expectEqual(0, eventCount); - + input.setSelectionRange(0, 5); input.select(); input.selectionStart = 3; diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index 53a0b07a..6d2244fb 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -365,6 +365,11 @@ pub fn createEvent(_: *const Document, event_type: []const u8, page: *Page) !*@i return (try KeyboardEvent.init("", null, page)).asEvent(); } + if (std.mem.eql(u8, normalized, "inputevent")) { + const InputEvent = @import("event/InputEvent.zig"); + return (try InputEvent.init("", null, page)).asEvent(); + } + if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) { const MouseEvent = @import("event/MouseEvent.zig"); return (try MouseEvent.init("", null, page)).asEvent(); diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index fc73d5c8..7cea6454 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -28,6 +28,7 @@ const HtmlElement = @import("../Html.zig"); const Form = @import("Form.zig"); const Selection = @import("../../Selection.zig"); const Event = @import("../../Event.zig"); +const InputEvent = @import("../../event/InputEvent.zig"); const Input = @This(); @@ -103,6 +104,11 @@ fn dispatchSelectionChangeEvent(self: *Input, page: *Page) !void { try page._event_manager.dispatch(self.asElement().asEventTarget(), event); } +fn dispatchInputEvent(self: *Input, data: ?[]const u8, input_type: []const u8, page: *Page) !void { + const event = try InputEvent.init("input", .{ .data = data, .inputType = input_type }, page); + try page._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent()); +} + pub fn asElement(self: *Input) *Element { return self._proto._proto; } @@ -425,6 +431,7 @@ pub fn innerInsert(self: *Input, str: []const u8, page: *Page) !void { try self.setValue(new_value, page); }, } + try self.dispatchInputEvent(str, "insertText", page); } pub fn getSelectionDirection(self: *const Input) []const u8 { diff --git a/src/browser/webapi/element/html/TextArea.zig b/src/browser/webapi/element/html/TextArea.zig index e08831a2..648e9754 100644 --- a/src/browser/webapi/element/html/TextArea.zig +++ b/src/browser/webapi/element/html/TextArea.zig @@ -26,6 +26,7 @@ const HtmlElement = @import("../Html.zig"); const Form = @import("Form.zig"); const Selection = @import("../../Selection.zig"); const Event = @import("../../Event.zig"); +const InputEvent = @import("../../event/InputEvent.zig"); const TextArea = @This(); @@ -55,6 +56,11 @@ fn dispatchSelectionChangeEvent(self: *TextArea, page: *Page) !void { try page._event_manager.dispatch(self.asElement().asEventTarget(), event); } +fn dispatchInputEvent(self: *TextArea, data: ?[]const u8, input_type: []const u8, page: *Page) !void { + const event = try InputEvent.init("input", .{ .data = data, .inputType = input_type }, page); + try page._event_manager.dispatch(self.asElement().asEventTarget(), event.asEvent()); +} + pub fn asElement(self: *TextArea) *Element { return self._proto._proto; } @@ -189,6 +195,7 @@ pub fn innerInsert(self: *TextArea, str: []const u8, page: *Page) !void { try self.setValue(new_value, page); }, } + try self.dispatchInputEvent(str, "insertText", page); } pub fn getSelectionDirection(self: *const TextArea) []const u8 { diff --git a/src/browser/webapi/event/InputEvent.zig b/src/browser/webapi/event/InputEvent.zig new file mode 100644 index 00000000..d81d51d6 --- /dev/null +++ b/src/browser/webapi/event/InputEvent.zig @@ -0,0 +1,121 @@ +// 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 String = @import("../../../string.zig").String; +const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); +const js = @import("../../js/js.zig"); + +const Event = @import("../Event.zig"); +const UIEvent = @import("UIEvent.zig"); +const Allocator = std.mem.Allocator; + +const InputEvent = @This(); + +_proto: *UIEvent, +_data: ?[]const u8, +// TODO: add dataTransfer +_input_type: []const u8, +_is_composing: bool, + +pub const InputEventOptions = struct { + data: ?[]const u8 = null, + inputType: ?[]const u8 = null, + isComposing: bool = false, +}; + +const Options = Event.inheritOptions( + InputEvent, + InputEventOptions, +); + +pub fn initTrusted(typ: String, _opts: ?Options, page: *Page) !*InputEvent { + const arena = try page.getArena(.{ .debug = "InputEvent.trusted" }); + errdefer page.releaseArena(arena); + return initWithTrusted(arena, typ, _opts, true, page); +} + +pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*InputEvent { + const arena = try page.getArena(.{ .debug = "InputEvent" }); + errdefer page.releaseArena(arena); + const type_string = try String.init(arena, typ, .{}); + return initWithTrusted(arena, type_string, _opts, false, page); +} + +fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, page: *Page) !*InputEvent { + const opts = _opts orelse Options{}; + + const event = try page._factory.uiEvent( + arena, + typ, + InputEvent{ + ._proto = undefined, + ._data = if (opts.data) |d| try arena.dupe(u8, d) else null, + ._input_type = if (opts.inputType) |it| try arena.dupe(u8, it) else "", + ._is_composing = opts.isComposing, + }, + ); + + Event.populatePrototypes(event, opts, trusted); + + // https://developer.mozilla.org/en-US/docs/Web/API/Element/input_event + const rootevt = event._proto._proto; + rootevt._bubbles = true; + rootevt._cancelable = false; + rootevt._composed = true; + + return event; +} + +pub fn deinit(self: *InputEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); +} + +pub fn asEvent(self: *InputEvent) *Event { + return self._proto.asEvent(); +} + +pub fn getData(self: *const InputEvent) ?[]const u8 { + return self._data; +} + +pub fn getInputType(self: *const InputEvent) []const u8 { + return self._input_type; +} + +pub fn getIsComposing(self: *const InputEvent) bool { + return self._is_composing; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(InputEvent); + + pub const Meta = struct { + pub const name = "InputEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + pub const weak = true; + pub const finalizer = bridge.finalizer(InputEvent.deinit); + }; + + pub const constructor = bridge.constructor(InputEvent.init, .{}); + pub const data = bridge.accessor(InputEvent.getData, null, .{}); + pub const inputType = bridge.accessor(InputEvent.getInputType, null, .{}); + pub const isComposing = bridge.accessor(InputEvent.getIsComposing, null, .{}); +}; diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig index 0aa2943b..bbd9a60f 100644 --- a/src/browser/webapi/event/UIEvent.zig +++ b/src/browser/webapi/event/UIEvent.zig @@ -37,6 +37,7 @@ pub const Type = union(enum) { keyboard_event: *@import("KeyboardEvent.zig"), focus_event: *@import("FocusEvent.zig"), text_event: *@import("TextEvent.zig"), + input_event: *@import("InputEvent.zig"), }; pub const UIEventOptions = struct { @@ -88,6 +89,7 @@ pub fn is(self: *UIEvent, comptime T: type) ?*T { .keyboard_event => |e| return if (T == @import("KeyboardEvent.zig")) e else null, .focus_event => |e| return if (T == @import("FocusEvent.zig")) e else null, .text_event => |e| return if (T == @import("TextEvent.zig")) e else null, + .input_event => |e| return if (T == @import("InputEvent.zig")) e else null, } return null; }