mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 04:34:44 +00:00
Add support for target attribute on anchors and forms
This commit is contained in:
@@ -558,12 +558,9 @@ fn scheduleNavigationWithArena(originator: *Page, arena: Allocator, request_url:
|
|||||||
};
|
};
|
||||||
|
|
||||||
const target = switch (nt) {
|
const target = switch (nt) {
|
||||||
|
.form, .anchor => |p| p,
|
||||||
.script => |p| p orelse originator,
|
.script => |p| p orelse originator,
|
||||||
.iframe => |iframe| iframe._window.?._page, // only an frame with existing content (i.e. a window) can be navigated
|
.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;
|
const session = target._session;
|
||||||
@@ -759,6 +756,10 @@ fn _documentIsComplete(self: *Page) !void {
|
|||||||
try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = "page show" });
|
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();
|
self.notifyParentLoadComplete();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3028,9 +3029,9 @@ const NavigationType = enum {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Navigation = union(NavigationType) {
|
const Navigation = union(NavigationType) {
|
||||||
form: *Node,
|
form: *Page,
|
||||||
script: ?*Page,
|
script: ?*Page,
|
||||||
anchor: *Node,
|
anchor: *Page,
|
||||||
iframe: *IFrame,
|
iframe: *IFrame,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -3042,6 +3043,69 @@ pub const QueuedNavigation = struct {
|
|||||||
navigation_type: NavigationType,
|
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 {
|
pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
|
||||||
const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return;
|
const target = (try self.window._document.elementFromPoint(x, y, self)) orelse return;
|
||||||
if (comptime IS_DEBUG) {
|
if (comptime IS_DEBUG) {
|
||||||
@@ -3080,29 +3144,27 @@ pub fn handleClick(self: *Page, target: *Node) !void {
|
|||||||
return;
|
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)) {
|
if (try element.hasAttribute(comptime .wrap("download"), self)) {
|
||||||
log.warn(.browser, "a.download", .{ .type = self._type, .url = self.url });
|
log.warn(.browser, "a.download", .{ .type = self._type, .url = self.url });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: We need to support targets properly, but this is the most
|
const target_page = blk: {
|
||||||
// common case: a click on an anchor navigates the page/frame that
|
const target_name = anchor.getTarget();
|
||||||
// anchor is in.
|
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 element.focus(self);
|
||||||
try self.scheduleNavigation(href, .{
|
try self.scheduleNavigation(href, .{
|
||||||
.reason = .script,
|
.reason = .script,
|
||||||
.kind = .{ .push = null },
|
.kind = .{ .push = null },
|
||||||
}, .{ .anchor = target });
|
}, .{ .anchor = target_page });
|
||||||
},
|
},
|
||||||
.input => |input| {
|
.input => |input| {
|
||||||
try element.focus(self);
|
try element.focus(self);
|
||||||
@@ -3195,6 +3257,25 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
|
|||||||
|
|
||||||
const form_element = form.asElement();
|
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) {
|
if (submit_opts.fire_event) {
|
||||||
const submit_event = try Event.initTrusted(comptime .wrap("submit"), .{ .bubbles = true, .cancelable = true }, self);
|
const submit_event = try Event.initTrusted(comptime .wrap("submit"), .{ .bubbles = true, .cancelable = true }, self);
|
||||||
|
|
||||||
@@ -3237,7 +3318,8 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
|
|||||||
} else {
|
} else {
|
||||||
action = try URL.concatQueryString(arena, action, buf.written());
|
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.
|
// 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>
|
||||||
@@ -492,6 +492,11 @@ pub fn ownerDocument(self: *const Node, page: *const Page) ?*Document {
|
|||||||
return 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 {
|
pub fn isSameDocumentAs(self: *const Node, other: *const Node, page: *const Page) bool {
|
||||||
// Get the root document for each node
|
// Get the root document for each node
|
||||||
const self_doc = if (self._type == .document) self._type.document else self.ownerDocument(page);
|
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);
|
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 {
|
pub fn getLength(self: *Form, page: *Page) !u32 {
|
||||||
const elements = try self.getElements(page);
|
const elements = try self.getElements(page);
|
||||||
return elements.length(page);
|
return elements.length(page);
|
||||||
@@ -120,6 +128,7 @@ pub const JsApi = struct {
|
|||||||
pub const name = bridge.accessor(Form.getName, Form.setName, .{});
|
pub const name = bridge.accessor(Form.getName, Form.setName, .{});
|
||||||
pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{});
|
pub const method = bridge.accessor(Form.getMethod, Form.setMethod, .{});
|
||||||
pub const action = bridge.accessor(Form.getAction, Form.setAction, .{});
|
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 elements = bridge.accessor(Form.getElements, null, .{});
|
||||||
pub const length = bridge.accessor(Form.getLength, null, .{});
|
pub const length = bridge.accessor(Form.getLength, null, .{});
|
||||||
pub const submit = bridge.function(Form.submit, .{});
|
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 JsApi = struct {
|
||||||
pub const bridge = js.Bridge(IFrame);
|
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 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 contentWindow = bridge.accessor(IFrame.getContentWindow, null, .{});
|
||||||
pub const contentDocument = bridge.accessor(IFrame.getContentDocument, null, .{});
|
pub const contentDocument = bridge.accessor(IFrame.getContentDocument, null, .{});
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user