mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-21 20:24:42 +00:00
Merge pull request #1780 from lightpanda-io/anchor_and_form_target
Add support for target attribute on anchors and forms
This commit is contained in:
@@ -562,12 +562,9 @@ fn scheduleNavigationWithArena(originator: *Page, arena: Allocator, request_url:
|
||||
};
|
||||
|
||||
const target = switch (nt) {
|
||||
.form, .anchor => |p| p,
|
||||
.script => |p| p orelse originator,
|
||||
.iframe => |iframe| iframe._window.?._page, // only an frame with existing content (i.e. a window) can be navigated
|
||||
.anchor, .form => |node| blk: {
|
||||
const doc = node.ownerDocument(originator) orelse break :blk originator;
|
||||
break :blk doc._page orelse originator;
|
||||
},
|
||||
};
|
||||
|
||||
const session = target._session;
|
||||
@@ -763,6 +760,10 @@ fn _documentIsComplete(self: *Page) !void {
|
||||
try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = "page show" });
|
||||
}
|
||||
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.page, "load", .{ .url = self.url, .type = self._type });
|
||||
}
|
||||
|
||||
self.notifyParentLoadComplete();
|
||||
}
|
||||
|
||||
@@ -3106,9 +3107,9 @@ const NavigationType = enum {
|
||||
};
|
||||
|
||||
const Navigation = union(NavigationType) {
|
||||
form: *Node,
|
||||
form: *Page,
|
||||
script: ?*Page,
|
||||
anchor: *Node,
|
||||
anchor: *Page,
|
||||
iframe: *IFrame,
|
||||
};
|
||||
|
||||
@@ -3120,6 +3121,69 @@ pub const QueuedNavigation = struct {
|
||||
navigation_type: NavigationType,
|
||||
};
|
||||
|
||||
/// Resolves a target attribute value (e.g., "_self", "_parent", "_top", or frame name)
|
||||
/// to the appropriate Page to navigate.
|
||||
/// Returns null if the target is "_blank" (which would open a new window/tab).
|
||||
/// Note: Callers should handle empty target separately (for owner document resolution).
|
||||
pub fn resolveTargetPage(self: *Page, target_name: []const u8) ?*Page {
|
||||
if (std.ascii.eqlIgnoreCase(target_name, "_self")) {
|
||||
return self;
|
||||
}
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(target_name, "_blank")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(target_name, "_parent")) {
|
||||
return self.parent orelse self;
|
||||
}
|
||||
|
||||
if (std.ascii.eqlIgnoreCase(target_name, "_top")) {
|
||||
var page = self;
|
||||
while (page.parent) |p| {
|
||||
page = p;
|
||||
}
|
||||
return page;
|
||||
}
|
||||
|
||||
// Named frame lookup: search current page's descendants first, then from root
|
||||
// This follows the HTML spec's "implementation-defined" search order.
|
||||
if (findFrameByName(self, target_name)) |frame_page| {
|
||||
return frame_page;
|
||||
}
|
||||
|
||||
// If not found in descendants, search from root (catches siblings and ancestors' descendants)
|
||||
var root = self;
|
||||
while (root.parent) |p| {
|
||||
root = p;
|
||||
}
|
||||
if (root != self) {
|
||||
if (findFrameByName(root, target_name)) |frame_page| {
|
||||
return frame_page;
|
||||
}
|
||||
}
|
||||
|
||||
// If no frame found with that name, navigate in current page
|
||||
// (this matches browser behavior - unknown targets act like _self)
|
||||
return self;
|
||||
}
|
||||
|
||||
fn findFrameByName(page: *Page, name: []const u8) ?*Page {
|
||||
for (page.frames.items) |frame| {
|
||||
if (frame.iframe) |iframe| {
|
||||
const frame_name = iframe.asElement().getAttributeSafe(comptime .wrap("name")) orelse "";
|
||||
if (std.mem.eql(u8, frame_name, name)) {
|
||||
return frame;
|
||||
}
|
||||
}
|
||||
// Recursively search child frames
|
||||
if (findFrameByName(frame, name)) |found| {
|
||||
return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
||||
const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return;
|
||||
if (comptime IS_DEBUG) {
|
||||
@@ -3158,29 +3222,27 @@ pub fn handleClick(self: *Page, target: *Node) !void {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check target attribute - don't navigate if opening in new window/tab
|
||||
const target_val = anchor.getTarget();
|
||||
if (target_val.len > 0 and !std.mem.eql(u8, target_val, "_self")) {
|
||||
log.warn(.not_implemented, "a.target", .{ .type = self._type, .url = self.url });
|
||||
return;
|
||||
}
|
||||
|
||||
if (try element.hasAttribute(comptime .wrap("download"), self)) {
|
||||
log.warn(.browser, "a.download", .{ .type = self._type, .url = self.url });
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: We need to support targets properly, but this is the most
|
||||
// common case: a click on an anchor navigates the page/frame that
|
||||
// anchor is in.
|
||||
const target_page = blk: {
|
||||
const target_name = anchor.getTarget();
|
||||
if (target_name.len == 0) {
|
||||
break :blk target.ownerPage(self);
|
||||
}
|
||||
break :blk self.resolveTargetPage(target_name) orelse {
|
||||
log.warn(.not_implemented, "target", .{ .type = self._type, .url = self.url, .target = target_name });
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
// ownerDocument only returns null when `target` is a document, which
|
||||
// it is NOT in this case. Even for a detched node, it'll return self.document
|
||||
try element.focus(self);
|
||||
try self.scheduleNavigation(href, .{
|
||||
.reason = .script,
|
||||
.kind = .{ .push = null },
|
||||
}, .{ .anchor = target });
|
||||
}, .{ .anchor = target_page });
|
||||
},
|
||||
.input => |input| {
|
||||
try element.focus(self);
|
||||
@@ -3273,6 +3335,25 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
|
||||
|
||||
const form_element = form.asElement();
|
||||
|
||||
const target_name_: ?[]const u8 = blk: {
|
||||
if (submitter_) |submitter| {
|
||||
if (submitter.getAttributeSafe(comptime .wrap("formtarget"))) |ft| {
|
||||
break :blk ft;
|
||||
}
|
||||
}
|
||||
break :blk form_element.getAttributeSafe(comptime .wrap("target"));
|
||||
};
|
||||
|
||||
const target_page = blk: {
|
||||
const target_name = target_name_ orelse {
|
||||
break :blk form_element.asNode().ownerPage(self);
|
||||
};
|
||||
break :blk self.resolveTargetPage(target_name) orelse {
|
||||
log.warn(.not_implemented, "target", .{ .type = self._type, .url = self.url, .target = target_name });
|
||||
return;
|
||||
};
|
||||
};
|
||||
|
||||
if (submit_opts.fire_event) {
|
||||
const submit_event = try Event.initTrusted(comptime .wrap("submit"), .{ .bubbles = true, .cancelable = true }, self);
|
||||
|
||||
@@ -3315,7 +3396,8 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
|
||||
} else {
|
||||
action = try URL.concatQueryString(arena, action, buf.written());
|
||||
}
|
||||
return self.scheduleNavigationWithArena(arena, action, opts, .{ .form = form_element.asNode() });
|
||||
|
||||
return self.scheduleNavigationWithArena(arena, action, opts, .{ .form = target_page });
|
||||
}
|
||||
|
||||
// insertText is a shortcut to insert text into the active element.
|
||||
|
||||
2
src/browser/tests/frames/support/page.html
Normal file
2
src/browser/tests/frames/support/page.html
Normal file
@@ -0,0 +1,2 @@
|
||||
<!DOCTYPE html>
|
||||
a-page
|
||||
42
src/browser/tests/frames/target.html
Normal file
42
src/browser/tests/frames/target.html
Normal file
@@ -0,0 +1,42 @@
|
||||
<!DOCTYPE html>
|
||||
<script src="../testing.js"></script>
|
||||
|
||||
<iframe name=f1 id=frame1></iframe>
|
||||
<a id=l1 target=f1 href=support/page.html></a>
|
||||
<script id=anchor>
|
||||
$('#l1').click();
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual('<html><head></head><body>a-page\n</body></html>', $('#frame1').contentDocument.documentElement.outerHTML);
|
||||
});
|
||||
</script>
|
||||
|
||||
<script id=form>
|
||||
{
|
||||
let frame2 = document.createElement('iframe');
|
||||
frame2.name = 'frame2';
|
||||
document.documentElement.appendChild(frame2);
|
||||
|
||||
let form = document.createElement('form');
|
||||
form.target = 'frame2';
|
||||
form.action = 'support/page.html';
|
||||
form.submit();
|
||||
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual('<html><head></head><body>a-page\n</body></html>', frame2.contentDocument.documentElement.outerHTML);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<iframe name=frame3 id=f3></iframe>
|
||||
<form target="_top" action="support/page.html">
|
||||
<input type=submit id=submit1 formtarget="frame3">
|
||||
</form>
|
||||
|
||||
<script id=formtarget>
|
||||
{
|
||||
$('#submit1').click();
|
||||
testing.eventually(() => {
|
||||
testing.expectEqual('<html><head></head><body>a-page\n</body></html>', $('#f3').contentDocument.documentElement.outerHTML);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
@@ -493,6 +493,11 @@ pub fn ownerDocument(self: *const Node, page: *const Page) ?*Document {
|
||||
return page.document;
|
||||
}
|
||||
|
||||
pub fn ownerPage(self: *const Node, default: *Page) *Page {
|
||||
const doc = self.ownerDocument(default) orelse return default;
|
||||
return doc._page orelse default;
|
||||
}
|
||||
|
||||
pub fn isSameDocumentAs(self: *const Node, other: *const Node, page: *const Page) bool {
|
||||
// Get the root document for each node
|
||||
const self_doc = if (self._type == .document) self._type.document else self.ownerDocument(page);
|
||||
|
||||
@@ -100,6 +100,14 @@ pub fn setAction(self: *Form, value: []const u8, page: *Page) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("action"), .wrap(value), page);
|
||||
}
|
||||
|
||||
pub fn getTarget(self: *Form) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("target")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setTarget(self: *Form, value: []const u8, page: *Page) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("target"), .wrap(value), page);
|
||||
}
|
||||
|
||||
pub fn getLength(self: *Form, page: *Page) !u32 {
|
||||
const elements = try self.getElements(page);
|
||||
return elements.length(page);
|
||||
@@ -120,6 +128,7 @@ pub const JsApi = struct {
|
||||
pub const name = bridge.accessor(Form.getName, Form.setName, .{});
|
||||
pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{});
|
||||
pub const action = bridge.accessor(Form.getAction, Form.setAction, .{});
|
||||
pub const target = bridge.accessor(Form.getTarget, Form.setTarget, .{});
|
||||
pub const elements = bridge.accessor(Form.getElements, null, .{});
|
||||
pub const length = bridge.accessor(Form.getLength, null, .{});
|
||||
pub const submit = bridge.function(Form.submit, .{});
|
||||
|
||||
@@ -65,6 +65,14 @@ pub fn setSrc(self: *IFrame, src: []const u8, page: *Page) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getName(self: *IFrame) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("name")) orelse "";
|
||||
}
|
||||
|
||||
pub fn setName(self: *IFrame, value: []const u8, page: *Page) !void {
|
||||
try self.asElement().setAttributeSafe(comptime .wrap("name"), .wrap(value), page);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(IFrame);
|
||||
|
||||
@@ -75,6 +83,7 @@ pub const JsApi = struct {
|
||||
};
|
||||
|
||||
pub const src = bridge.accessor(IFrame.getSrc, IFrame.setSrc, .{});
|
||||
pub const name = bridge.accessor(IFrame.getName, IFrame.setName, .{});
|
||||
pub const contentWindow = bridge.accessor(IFrame.getContentWindow, null, .{});
|
||||
pub const contentDocument = bridge.accessor(IFrame.getContentDocument, null, .{});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user