diff --git a/src/browser/Session.zig b/src/browser/Session.zig index c9412e64a..f6f0653ca 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -86,6 +86,10 @@ _nav_cursor: usize = 0, // `commitPendingPage`). _tool_frame_override: ?u32 = null, +// A popup the last tool action opened (target=_blank). Tools act on it, as +// a user whose click opened a tab would, until it goes away. +_followed_popup: ?u32 = null, + // Loader IDs are scoped to the Session: each new BrowserContext gets a // fresh counter. Frame IDs (`frame_id_gen`) live on `Browser` instead so // CDP target IDs stay unique across BrowserContext lifecycle on a single @@ -457,6 +461,12 @@ pub fn currentFrame(self: *Session) ?*Frame { // No pages[0] fallthrough: the override targets one specific page. return self.findFrameByFrameId(frame_id); } + if (self._followed_popup) |frame_id| { + if (self.findFrameByFrameId(frame_id)) |frame| { + return frame; + } + self._followed_popup = null; + } if (self.pages.items.len == 0) { return null; } @@ -472,6 +482,11 @@ pub fn setToolFrameOverride(self: *Session, frame_id: ?u32) void { self._tool_frame_override = frame_id; } +/// See `_followed_popup`. +pub fn followPopup(self: *Session, frame_id: u32) void { + self._followed_popup = frame_id; +} + // Multi-page aware: frame ids are globally unique (monotonic on `Browser`). // First we find the "live" page for a frame, then we search every nested // frame within that page. diff --git a/src/browser/frame/user_input.zig b/src/browser/frame/user_input.zig index c074caf44..b3f78287a 100644 --- a/src/browser/frame/user_input.zig +++ b/src/browser/frame/user_input.zig @@ -553,7 +553,15 @@ fn followLink(frame: *Frame, target: *Node, element: *Element, href: []const u8, break :blk target.ownerFrame(frame); } break :blk frame.resolveTargetFrame(target_name) orelse { - log.warn(.not_implemented, "target", .{ .type = frame._type, .url = frame.url, .target = target_name }); + // _blank: a new top-level context, as window.open creates. The + // opener is withheld unless rel=opener. + const owner = target.ownerFrame(frame); + try element.focus(frame); + _ = try owner.openPopup(.{ + .url = href, + .name = "", + .opener = if (hasRelToken(element, "opener")) owner.window else null, + }); return; }; }; @@ -565,6 +573,17 @@ fn followLink(frame: *Frame, target: *Node, element: *Element, href: []const u8, }, .{ .anchor = target_frame }); } +fn hasRelToken(element: *Element, token: []const u8) bool { + const rel = element.getAttributeSafe(comptime .wrap("rel")) orelse return false; + var it = std.mem.tokenizeAny(u8, rel, &std.ascii.whitespace); + while (it.next()) |t| { + if (std.ascii.eqlIgnoreCase(t, token)) { + return true; + } + } + return false; +} + pub fn triggerKeyboard(frame: *Frame, keyboard_event: *KeyboardEvent) !void { const event = keyboard_event.asEvent(); // Dispatch to the effective active element. When nothing is explicitly diff --git a/src/browser/tests/frames/target.html b/src/browser/tests/frames/target.html index a44a47cb3..f15fdb144 100644 --- a/src/browser/tests/frames/target.html +++ b/src/browser/tests/frames/target.html @@ -38,6 +38,13 @@ } + + +