Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions src/browser/dom/document.zig
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ pub const Document = struct {
pub const prototype = *Node;
pub const subtype = .node;

active_element: ?*parser.Element = null,

pub fn constructor(page: *const Page) !*parser.DocumentHTML {
const doc = try parser.documentCreateDocument(
try parser.documentHTMLGetTitle(page.window.document),
Expand Down Expand Up @@ -242,6 +244,19 @@ pub const Document = struct {
pub fn _createTreeWalker(_: *parser.Document, root: *parser.Node, what_to_show: ?u32, filter: ?TreeWalker.TreeWalkerOpts) !TreeWalker {
return try TreeWalker.init(root, what_to_show, filter);
}

pub fn get_activeElement(doc: *parser.Document, page: *Page) !?ElementUnion {
const self = try page.getOrCreateNodeWrapper(Document, @ptrCast(doc));
if (self.active_element) |ae| {
return try Element.toInterface(ae);
}

if (try parser.documentHTMLBody(page.window.document)) |body| {
return try Element.toInterface(@ptrCast(body));
}

return get_documentElement(doc);
}
};

const testing = @import("../../testing.zig");
Expand Down Expand Up @@ -412,6 +427,12 @@ test "Browser.DOM.Document" {
},
}, .{});

try runner.testCases(&.{
.{ "document.activeElement === document.body", "true" },
.{ "document.getElementById('link').focus()", "undefined" },
.{ "document.activeElement === document.getElementById('link')", "true" },
}, .{});

// this test breaks the doc structure, keep it at the end of the test
// suite.
try runner.testCases(&.{
Expand Down
6 changes: 2 additions & 4 deletions src/browser/dom/element.zig
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,7 @@ pub const Element = struct {
// Returns a 0 DOMRect object if the element is eventually detached from the main window
pub fn _getBoundingClientRect(self: *parser.Element, page: *Page) !DOMRect {
// Since we are lazy rendering we need to do this check. We could store the renderer in a viewport such that it could cache these, but it would require tracking changes.
const root = try parser.nodeGetRootNode(parser.elementToNode(self));
if (root != parser.documentToNode(parser.documentHTMLToDocument(page.window.document))) {
if (!try page.isNodeAttached(parser.elementToNode(self))) {
return DOMRect{ .x = 0, .y = 0, .width = 0, .height = 0 };
}
return page.renderer.getRect(self);
Expand All @@ -379,8 +378,7 @@ pub const Element = struct {
// We do not render so it only always return the element's bounding rect.
// Returns an empty array if the element is eventually detached from the main window
pub fn _getClientRects(self: *parser.Element, page: *Page) ![]DOMRect {
const root = try parser.nodeGetRootNode(parser.elementToNode(self));
if (root != parser.documentToNode(parser.documentHTMLToDocument(page.window.document))) {
if (!try page.isNodeAttached(parser.elementToNode(self))) {
return &.{};
}
const heap_ptr = try page.call_arena.create(DOMRect);
Expand Down
28 changes: 28 additions & 0 deletions src/browser/html/elements.zig
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,27 @@ pub const HTMLElement = struct {
});
_ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
}

const FocusOpts = struct {
preventScroll: bool,
focusVisible: bool,
};
pub fn _focus(e: *parser.ElementHTML, _: ?FocusOpts, page: *Page) !void {
if (!try page.isNodeAttached(@ptrCast(e))) {
return;
}

const root_node = try parser.nodeGetRootNode(@ptrCast(e));

const Document = @import("../dom/document.zig").Document;
const document = try page.getOrCreateNodeWrapper(Document, @ptrCast(root_node));

// TODO: some elements can't be focused, like if they're disabled
// but there doesn't seem to be a generic way to check this. For example
// we could look for the "disabled" attribute, but that's only meaningful
// on certain types, and libdom's vtable doesn't seem to expose this.
document.active_element = @ptrCast(e);
}
};

// Deprecated HTMLElements in Chrome (2023/03/15)
Expand Down Expand Up @@ -1181,4 +1202,11 @@ test "Browser.HTML.Element" {
.{ "a.href = 'about'", null },
.{ "a.href", "https://lightpanda.io/opensource-browser/about" },
}, .{});

// detached node cannot be focused
try runner.testCases(&.{
.{ "const focused = document.activeElement", null },
.{ "document.createElement('a').focus()", null },
.{ "document.activeElement === focused", "true" },
}, .{});
}
5 changes: 5 additions & 0 deletions src/browser/page.zig
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,11 @@ pub const Page = struct {
try self.navigateFromWebAPI(action, opts);
}

pub fn isNodeAttached(self: *const Page, node: *parser.Node) !bool {
const root = parser.documentToNode(parser.documentHTMLToDocument(self.window.document));
return root == try parser.nodeGetRootNode(node);
}

fn elementSubmitForm(self: *Page, element: *parser.Element) !void {
const form = (try self.formForElement(element)) orelse return;
return self.submitForm(@ptrCast(form), @ptrCast(element));
Expand Down