mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Add basic support for key events
Support CDP's Input.dispatchKeyEvent and DOM key events. Currently only keydown is supported and expects every key to be a displayable character. It turns out that manipulating the DOM via key events isn't great because the behavior really depends on the cursor. So, to do this more accurately, we'd have to introduce some concept of a cursor. Personally, I don't think we'll run into many pages that are purposefully using keyboard events. But driver (puppeteer/playwright) scripts might be another issue.
This commit is contained in:
@@ -243,17 +243,23 @@ pub const Document = struct {
|
|||||||
return try TreeWalker.init(root, what_to_show, filter);
|
return try TreeWalker.init(root, what_to_show, filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
|
pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element {
|
||||||
const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self)));
|
if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| {
|
||||||
if (state.active_element) |ae| {
|
if (state.active_element) |ae| {
|
||||||
return try Element.toInterface(ae);
|
return ae;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
if (try parser.documentHTMLBody(page.window.document)) |body| {
|
||||||
return try Element.toInterface(@alignCast(@ptrCast(body)));
|
return @alignCast(@ptrCast(body));
|
||||||
}
|
}
|
||||||
|
|
||||||
return get_documentElement(self);
|
return try parser.documentGetDocumentElement(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion {
|
||||||
|
const ae = (try getActiveElement(self, page)) orelse return null;
|
||||||
|
return try Element.toInterface(ae);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: some elements can't be focused, like if they're disabled
|
// TODO: some elements can't be focused, like if they're disabled
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const c = @cImport({
|
|||||||
@cInclude("events/event_target.h");
|
@cInclude("events/event_target.h");
|
||||||
@cInclude("events/event.h");
|
@cInclude("events/event.h");
|
||||||
@cInclude("events/mouse_event.h");
|
@cInclude("events/mouse_event.h");
|
||||||
|
@cInclude("events/keyboard_event.h");
|
||||||
@cInclude("utils/validate.h");
|
@cInclude("utils/validate.h");
|
||||||
@cInclude("html/html_element.h");
|
@cInclude("html/html_element.h");
|
||||||
@cInclude("html/html_document.h");
|
@cInclude("html/html_document.h");
|
||||||
@@ -864,6 +865,59 @@ pub fn mouseEventDefaultPrevented(evt: *MouseEvent) !bool {
|
|||||||
return eventDefaultPrevented(@ptrCast(evt));
|
return eventDefaultPrevented(@ptrCast(evt));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// KeyboardEvent
|
||||||
|
|
||||||
|
pub const KeyboardEvent = c.dom_keyboard_event;
|
||||||
|
|
||||||
|
pub fn keyboardEventCreate() !*KeyboardEvent {
|
||||||
|
var evt: ?*KeyboardEvent = undefined;
|
||||||
|
const err = c._dom_keyboard_event_create(&evt);
|
||||||
|
try DOMErr(err);
|
||||||
|
return evt.?;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keyboardEventDestroy(evt: *KeyboardEvent) void {
|
||||||
|
c._dom_keyboard_event_destroy(evt);
|
||||||
|
}
|
||||||
|
|
||||||
|
const KeyboardEventOpts = struct {
|
||||||
|
key: []const u8,
|
||||||
|
code: []const u8,
|
||||||
|
bubbles: bool = false,
|
||||||
|
cancelable: bool = false,
|
||||||
|
ctrl: bool = false,
|
||||||
|
alt: bool = false,
|
||||||
|
shift: bool = false,
|
||||||
|
meta: bool = false,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn keyboardEventInit(evt: *KeyboardEvent, typ: []const u8, opts: KeyboardEventOpts) !void {
|
||||||
|
const s = try strFromData(typ);
|
||||||
|
const err = c._dom_keyboard_event_init(
|
||||||
|
evt,
|
||||||
|
s,
|
||||||
|
opts.bubbles,
|
||||||
|
opts.cancelable,
|
||||||
|
null, // dom_abstract_view* ?
|
||||||
|
try strFromData(opts.key),
|
||||||
|
try strFromData(opts.code),
|
||||||
|
0, // location 0 == standard
|
||||||
|
opts.ctrl,
|
||||||
|
opts.shift,
|
||||||
|
opts.alt,
|
||||||
|
opts.meta,
|
||||||
|
false, // repease
|
||||||
|
false, // is_composiom
|
||||||
|
);
|
||||||
|
try DOMErr(err);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn keyboardEventGetKey(evt: *KeyboardEvent) ![]const u8 {
|
||||||
|
var s: ?*String = undefined;
|
||||||
|
_ = c._dom_keyboard_event_get_key(evt, &s);
|
||||||
|
return strToData(s.?);
|
||||||
|
}
|
||||||
|
|
||||||
// NodeType
|
// NodeType
|
||||||
|
|
||||||
pub const NodeType = enum(u4) {
|
pub const NodeType = enum(u4) {
|
||||||
@@ -2393,6 +2447,11 @@ pub fn textareaGetValue(textarea: *TextArea) ![]const u8 {
|
|||||||
return strToData(s);
|
return strToData(s);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn textareaSetValue(textarea: *TextArea, value: []const u8) !void {
|
||||||
|
const err = c.dom_html_text_area_element_set_value(textarea, try strFromData(value));
|
||||||
|
try DOMErr(err);
|
||||||
|
}
|
||||||
|
|
||||||
// Select
|
// Select
|
||||||
pub fn selectGetOptions(select: *Select) !*OptionCollection {
|
pub fn selectGetOptions(select: *Select) !*OptionCollection {
|
||||||
var collection: ?*OptionCollection = null;
|
var collection: ?*OptionCollection = null;
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ pub const Page = struct {
|
|||||||
|
|
||||||
microtask_node: Loop.CallbackNode,
|
microtask_node: Loop.CallbackNode,
|
||||||
|
|
||||||
|
keydown_event_node: parser.EventNode,
|
||||||
window_clicked_event_node: parser.EventNode,
|
window_clicked_event_node: parser.EventNode,
|
||||||
|
|
||||||
// Our JavaScript context for this specific page. This is what we use to
|
// Our JavaScript context for this specific page. This is what we use to
|
||||||
@@ -112,6 +113,7 @@ pub const Page = struct {
|
|||||||
.state_pool = &browser.state_pool,
|
.state_pool = &browser.state_pool,
|
||||||
.cookie_jar = &session.cookie_jar,
|
.cookie_jar = &session.cookie_jar,
|
||||||
.microtask_node = .{ .func = microtaskCallback },
|
.microtask_node = .{ .func = microtaskCallback },
|
||||||
|
.keydown_event_node = .{ .func = keydownCallback },
|
||||||
.window_clicked_event_node = .{ .func = windowClicked },
|
.window_clicked_event_node = .{ .func = windowClicked },
|
||||||
.request_factory = browser.http_client.requestFactory(.{
|
.request_factory = browser.http_client.requestFactory(.{
|
||||||
.notification = browser.notification,
|
.notification = browser.notification,
|
||||||
@@ -307,6 +309,12 @@ pub const Page = struct {
|
|||||||
&self.window_clicked_event_node,
|
&self.window_clicked_event_node,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
_ = try parser.eventTargetAddEventListener(
|
||||||
|
parser.toEventTarget(parser.Element, document_element),
|
||||||
|
"keydown",
|
||||||
|
&self.keydown_event_node,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
// https://html.spec.whatwg.org/#read-html
|
// https://html.spec.whatwg.org/#read-html
|
||||||
|
|
||||||
@@ -595,6 +603,76 @@ pub const Page = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const KeyboardEvent = struct {
|
||||||
|
type: Type,
|
||||||
|
key: []const u8,
|
||||||
|
code: []const u8,
|
||||||
|
alt: bool,
|
||||||
|
ctrl: bool,
|
||||||
|
meta: bool,
|
||||||
|
shift: bool,
|
||||||
|
|
||||||
|
const Type = enum {
|
||||||
|
keydown,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn keyboardEvent(self: *Page, kbe: KeyboardEvent) !void {
|
||||||
|
if (kbe.type != .keydown) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Document = @import("dom/document.zig").Document;
|
||||||
|
const element = (try Document.getActiveElement(@ptrCast(self.window.document), self)) orelse return;
|
||||||
|
|
||||||
|
const event = try parser.keyboardEventCreate();
|
||||||
|
defer parser.keyboardEventDestroy(event);
|
||||||
|
try parser.keyboardEventInit(event, "keydown", .{
|
||||||
|
.bubbles = true,
|
||||||
|
.cancelable = true,
|
||||||
|
.key = kbe.key,
|
||||||
|
.code = kbe.code,
|
||||||
|
.alt = kbe.alt,
|
||||||
|
.ctrl = kbe.ctrl,
|
||||||
|
.meta = kbe.meta,
|
||||||
|
.shift = kbe.shift,
|
||||||
|
});
|
||||||
|
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
|
||||||
|
}
|
||||||
|
|
||||||
|
fn keydownCallback(node: *parser.EventNode, event: *parser.Event) void {
|
||||||
|
const self: *Page = @fieldParentPtr("keydown_event_node", node);
|
||||||
|
self._keydownCallback(event) catch |err| {
|
||||||
|
log.err(.browser, "keydown handler error", .{ .err = err });
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _keydownCallback(page: *Page, event: *parser.Event) !void {
|
||||||
|
const kbe: *parser.KeyboardEvent = @ptrCast(event);
|
||||||
|
const target = (try parser.eventTarget(event)) orelse return;
|
||||||
|
const node = parser.eventTargetToNode(target);
|
||||||
|
const tag = (try parser.nodeHTMLGetTagType(node)) orelse return;
|
||||||
|
switch (tag) {
|
||||||
|
.input => {
|
||||||
|
const element: *parser.Element = @ptrCast(node);
|
||||||
|
const input_type = (try parser.elementGetAttribute(element, "type")) orelse "text";
|
||||||
|
if (std.mem.eql(u8, input_type, "text")) {
|
||||||
|
const value = try parser.inputGetValue(@ptrCast(element));
|
||||||
|
const new_key = try parser.keyboardEventGetKey(kbe);
|
||||||
|
const new_value = try std.mem.concat(page.arena, u8, &.{ value, new_key });
|
||||||
|
try parser.inputSetValue(@ptrCast(element), new_value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
.textarea => {
|
||||||
|
const value = try parser.textareaGetValue(@ptrCast(node));
|
||||||
|
const new_key = try parser.keyboardEventGetKey(kbe);
|
||||||
|
const new_value = try std.mem.concat(page.arena, u8, &.{ value, new_key });
|
||||||
|
try parser.textareaSetValue(@ptrCast(node), new_value);
|
||||||
|
},
|
||||||
|
else => {},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// As such we schedule the function to be called as soon as possible.
|
// As such we schedule the function to be called as soon as possible.
|
||||||
// The page.arena is safe to use here, but the transfer_arena exists
|
// The page.arena is safe to use here, but the transfer_arena exists
|
||||||
// specifically for this type of lifetime.
|
// specifically for this type of lifetime.
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
|
|||||||
}
|
}
|
||||||
submitter_included = true;
|
submitter_included = true;
|
||||||
}
|
}
|
||||||
const value = (try parser.elementGetAttribute(element, "value")) orelse "";
|
const value = try parser.inputGetValue(@ptrCast(element));
|
||||||
try entries.appendOwned(arena, name, value);
|
try entries.appendOwned(arena, name, value);
|
||||||
},
|
},
|
||||||
.select => {
|
.select => {
|
||||||
@@ -189,11 +189,11 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (submitter_included == false) {
|
if (submitter_included == false) {
|
||||||
if (submitter_) |submitter| {
|
if (submitter_name_) |submitter_name| {
|
||||||
// this can happen if the submitter is outside the form, but associated
|
// this can happen if the submitter is outside the form, but associated
|
||||||
// with the form via a form=ID attribute
|
// with the form via a form=ID attribute
|
||||||
const value = (try parser.elementGetAttribute(@ptrCast(submitter), "value")) orelse "";
|
const value = (try parser.elementGetAttribute(@ptrCast(submitter_.?), "value")) orelse "";
|
||||||
try entries.appendOwned(arena, submitter_name_.?, value);
|
try entries.appendOwned(arena, submitter_name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,14 +21,60 @@ const Page = @import("../../browser/page.zig").Page;
|
|||||||
|
|
||||||
pub fn processMessage(cmd: anytype) !void {
|
pub fn processMessage(cmd: anytype) !void {
|
||||||
const action = std.meta.stringToEnum(enum {
|
const action = std.meta.stringToEnum(enum {
|
||||||
|
dispatchKeyEvent,
|
||||||
dispatchMouseEvent,
|
dispatchMouseEvent,
|
||||||
}, cmd.input.action) orelse return error.UnknownMethod;
|
}, cmd.input.action) orelse return error.UnknownMethod;
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
|
.dispatchKeyEvent => return dispatchKeyEvent(cmd),
|
||||||
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
|
.dispatchMouseEvent => return dispatchMouseEvent(cmd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
|
||||||
|
fn dispatchKeyEvent(cmd: anytype) !void {
|
||||||
|
const params = (try cmd.params(struct {
|
||||||
|
type: Type,
|
||||||
|
key: []const u8,
|
||||||
|
code: []const u8,
|
||||||
|
modifiers: u4,
|
||||||
|
// Many optional parameters are not implemented yet, see documentation url.
|
||||||
|
|
||||||
|
const Type = enum {
|
||||||
|
keyDown,
|
||||||
|
keyUp,
|
||||||
|
rawKeyDown,
|
||||||
|
char,
|
||||||
|
};
|
||||||
|
})) orelse return error.InvalidParams;
|
||||||
|
|
||||||
|
try cmd.sendResult(null, .{});
|
||||||
|
|
||||||
|
// quickly ignore types we know we don't handle
|
||||||
|
switch (params.type) {
|
||||||
|
.keyUp, .rawKeyDown, .char => return,
|
||||||
|
.keyDown => {},
|
||||||
|
}
|
||||||
|
|
||||||
|
const bc = cmd.browser_context orelse return;
|
||||||
|
const page = bc.session.currentPage() orelse return;
|
||||||
|
|
||||||
|
const keyboard_event = Page.KeyboardEvent{
|
||||||
|
.key = params.key,
|
||||||
|
.code = params.code,
|
||||||
|
.type = switch (params.type) {
|
||||||
|
.keyDown => .keydown,
|
||||||
|
else => unreachable,
|
||||||
|
},
|
||||||
|
.alt = params.modifiers & 1 == 1,
|
||||||
|
.ctrl = params.modifiers & 2 == 2,
|
||||||
|
.meta = params.modifiers & 4 == 4,
|
||||||
|
.shift = params.modifiers & 8 == 8,
|
||||||
|
};
|
||||||
|
try page.keyboardEvent(keyboard_event);
|
||||||
|
// result already sent
|
||||||
|
}
|
||||||
|
|
||||||
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
|
// https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
|
||||||
fn dispatchMouseEvent(cmd: anytype) !void {
|
fn dispatchMouseEvent(cmd: anytype) !void {
|
||||||
const params = (try cmd.params(struct {
|
const params = (try cmd.params(struct {
|
||||||
|
|||||||
Reference in New Issue
Block a user