diff --git a/src/browser/dom/document.zig b/src/browser/dom/document.zig index 569c1cfc..bc8dccbc 100644 --- a/src/browser/dom/document.zig +++ b/src/browser/dom/document.zig @@ -243,17 +243,23 @@ pub const Document = struct { return try TreeWalker.init(root, what_to_show, filter); } - pub fn get_activeElement(self: *parser.Document, page: *Page) !?ElementUnion { - const state = try page.getOrCreateNodeState(@alignCast(@ptrCast(self))); - if (state.active_element) |ae| { - return try Element.toInterface(ae); + pub fn getActiveElement(self: *parser.Document, page: *Page) !?*parser.Element { + if (page.getNodeState(@alignCast(@ptrCast(self)))) |state| { + if (state.active_element) |ae| { + return ae; + } } 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 diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index 0dca5ba0..55fd361a 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -25,6 +25,7 @@ const c = @cImport({ @cInclude("events/event_target.h"); @cInclude("events/event.h"); @cInclude("events/mouse_event.h"); + @cInclude("events/keyboard_event.h"); @cInclude("utils/validate.h"); @cInclude("html/html_element.h"); @cInclude("html/html_document.h"); @@ -864,6 +865,59 @@ pub fn mouseEventDefaultPrevented(evt: *MouseEvent) !bool { 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 pub const NodeType = enum(u4) { @@ -2393,6 +2447,11 @@ pub fn textareaGetValue(textarea: *TextArea) ![]const u8 { 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 pub fn selectGetOptions(select: *Select) !*OptionCollection { var collection: ?*OptionCollection = null; diff --git a/src/browser/page.zig b/src/browser/page.zig index e69d9d37..0169c86a 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -80,6 +80,7 @@ pub const Page = struct { microtask_node: Loop.CallbackNode, + keydown_event_node: parser.EventNode, window_clicked_event_node: parser.EventNode, // 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, .cookie_jar = &session.cookie_jar, .microtask_node = .{ .func = microtaskCallback }, + .keydown_event_node = .{ .func = keydownCallback }, .window_clicked_event_node = .{ .func = windowClicked }, .request_factory = browser.http_client.requestFactory(.{ .notification = browser.notification, @@ -307,6 +309,12 @@ pub const Page = struct { &self.window_clicked_event_node, false, ); + _ = try parser.eventTargetAddEventListener( + parser.toEventTarget(parser.Element, document_element), + "keydown", + &self.keydown_event_node, + false, + ); // 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. // The page.arena is safe to use here, but the transfer_arena exists // specifically for this type of lifetime. diff --git a/src/browser/xhr/form_data.zig b/src/browser/xhr/form_data.zig index 56e4131b..c5e3f278 100644 --- a/src/browser/xhr/form_data.zig +++ b/src/browser/xhr/form_data.zig @@ -162,7 +162,7 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page } submitter_included = true; } - const value = (try parser.elementGetAttribute(element, "value")) orelse ""; + const value = try parser.inputGetValue(@ptrCast(element)); try entries.appendOwned(arena, name, value); }, .select => { @@ -189,11 +189,11 @@ fn collectForm(form: *parser.Form, submitter_: ?*parser.ElementHTML, page: *Page } if (submitter_included == false) { - if (submitter_) |submitter| { + if (submitter_name_) |submitter_name| { // this can happen if the submitter is outside the form, but associated // with the form via a form=ID attribute - const value = (try parser.elementGetAttribute(@ptrCast(submitter), "value")) orelse ""; - try entries.appendOwned(arena, submitter_name_.?, value); + const value = (try parser.elementGetAttribute(@ptrCast(submitter_.?), "value")) orelse ""; + try entries.appendOwned(arena, submitter_name, value); } } diff --git a/src/cdp/domains/input.zig b/src/cdp/domains/input.zig index 170098ff..0a25584a 100644 --- a/src/cdp/domains/input.zig +++ b/src/cdp/domains/input.zig @@ -21,14 +21,60 @@ const Page = @import("../../browser/page.zig").Page; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { + dispatchKeyEvent, dispatchMouseEvent, }, cmd.input.action) orelse return error.UnknownMethod; switch (action) { + .dispatchKeyEvent => return dispatchKeyEvent(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 fn dispatchMouseEvent(cmd: anytype) !void { const params = (try cmd.params(struct {