From e74d7fa454d08e32350ed0d722b1f2594faddc00 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Wed, 24 Sep 2025 00:20:34 -0700 Subject: [PATCH] add popstate event for History --- src/browser/events/event.zig | 3 ++ src/browser/fetch/fetch.zig | 7 +--- src/browser/html/History.zig | 76 ++++++++++++++++++++++++++++++++++++ src/browser/netsurf.zig | 1 + src/browser/page.zig | 5 +++ src/tests/html/history.html | 14 ++++++- 6 files changed, 98 insertions(+), 8 deletions(-) diff --git a/src/browser/events/event.zig b/src/browser/events/event.zig index 0754a6ad..ef039fac 100644 --- a/src/browser/events/event.zig +++ b/src/browser/events/event.zig @@ -36,6 +36,7 @@ const MouseEvent = @import("mouse_event.zig").MouseEvent; const KeyboardEvent = @import("keyboard_event.zig").KeyboardEvent; const ErrorEvent = @import("../html/error_event.zig").ErrorEvent; const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent; +const PopStateEvent = @import("../html/History.zig").PopStateEvent; // Event interfaces pub const Interfaces = .{ @@ -46,6 +47,7 @@ pub const Interfaces = .{ KeyboardEvent, ErrorEvent, MessageEvent, + PopStateEvent, }; pub const Union = generate.Union(Interfaces); @@ -73,6 +75,7 @@ pub const Event = struct { .error_event => .{ .ErrorEvent = @as(*ErrorEvent, @ptrCast(evt)).* }, .message_event => .{ .MessageEvent = @as(*MessageEvent, @ptrCast(evt)).* }, .keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) }, + .pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* }, }; } diff --git a/src/browser/fetch/fetch.zig b/src/browser/fetch/fetch.zig index 9dd073df..e1e29a81 100644 --- a/src/browser/fetch/fetch.zig +++ b/src/browser/fetch/fetch.zig @@ -64,7 +64,7 @@ pub const FetchContext = struct { var headers: Headers = .{}; // seems to be the highest priority - const same_origin = try isSameOriginAsPage(self.url, self.page); + const same_origin = try self.page.isSameOrigin(self.url); // If the mode is "no-cors", we need to return this opaque/stripped Response. // https://developer.mozilla.org/en-US/docs/Web/API/Response/type @@ -237,11 +237,6 @@ pub fn fetch(input: RequestInput, options: ?RequestInit, page: *Page) !Env.Promi return resolver.promise(); } -fn isSameOriginAsPage(url: []const u8, page: *const Page) !bool { - const origin = try page.origin(page.call_arena); - return std.mem.startsWith(u8, url, origin); -} - const testing = @import("../../testing.zig"); test "fetch: fetch" { try testing.htmlRunner("fetch/fetch.html"); diff --git a/src/browser/html/History.zig b/src/browser/html/History.zig index 9e5428fd..8931d7bb 100644 --- a/src/browser/html/History.zig +++ b/src/browser/html/History.zig @@ -17,6 +17,7 @@ // along with this program. If not, see . const std = @import("std"); +const log = @import("../../log.zig"); const Env = @import("../env.zig").Env; const Page = @import("../page.zig").Page; @@ -87,6 +88,32 @@ pub fn pushNavigation(self: *History, _url: []const u8, page: *Page) !void { self.current = self.stack.items.len - 1; } +pub fn dispatchPopStateEvent(state: ?[]const u8, page: *Page) void { + log.debug(.script_event, "dispatch popstate event", .{ + .type = "popstate", + .source = "history", + }); + History._dispatchPopStateEvent(state, page) catch |err| { + log.err(.app, "dispatch popstate event error", .{ + .err = err, + .type = "popstate", + .source = "history", + }); + }; +} + +fn _dispatchPopStateEvent( + state: ?[]const u8, + page: *Page, +) !void { + var evt = try PopStateEvent.constructor("popstate", .{ .state = state }); + + _ = try parser.eventTargetDispatchEvent( + @as(*parser.EventTarget, @ptrCast(&page.window)), + @as(*parser.Event, @ptrCast(&evt)), + ); +} + pub fn _pushState(self: *History, state: Env.JsObject, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void { const arena = page.session.arena; @@ -123,6 +150,15 @@ pub fn go(self: *History, delta: i32, page: *Page) !void { const index = @as(usize, @intCast(index_s)); const entry = self.stack.items[index]; self.current = index; + + if (try page.isSameOrigin(entry.url)) { + if (entry.state) |state| { + History.dispatchPopStateEvent(state, page); + } else { + History.dispatchPopStateEvent(null, page); + } + } + try page.navigateFromWebAPI(entry.url, .{ .reason = .history }); } @@ -138,6 +174,46 @@ pub fn _forward(self: *History, page: *Page) !void { try self.go(1, page); } +const parser = @import("../netsurf.zig"); +const Event = @import("../events/event.zig").Event; + +pub const PopStateEvent = struct { + pub const prototype = *Event; + pub const union_make_copy = true; + + pub const EventInit = struct { + state: ?[]const u8 = null, + }; + + proto: parser.Event, + state: ?[]const u8, + + pub fn constructor(event_type: []const u8, opts: ?EventInit) !PopStateEvent { + const event = try parser.eventCreate(); + defer parser.eventDestroy(event); + try parser.eventInit(event, event_type, .{}); + parser.eventSetInternalType(event, .pop_state); + + const o = opts orelse EventInit{}; + + return .{ + .proto = event.*, + .state = o.state, + }; + } + + // `hasUAVisualTransition` is not implemented. It isn't baseline so this is okay. + + pub fn get_state(self: *const PopStateEvent, page: *Page) !?Env.Value { + if (self.state) |state| { + const value = try Env.Value.fromJson(page.main_context, state); + return value; + } else { + return null; + } + } +}; + const testing = @import("../../testing.zig"); test "Browser: HTML.History" { try testing.htmlRunner("html/history.html"); diff --git a/src/browser/netsurf.zig b/src/browser/netsurf.zig index 823d6546..4f30f6cd 100644 --- a/src/browser/netsurf.zig +++ b/src/browser/netsurf.zig @@ -558,6 +558,7 @@ pub const EventType = enum(u8) { xhr_event = 6, message_event = 7, keyboard_event = 8, + pop_state = 9, }; pub const MutationEvent = c.dom_mutation_event; diff --git a/src/browser/page.zig b/src/browser/page.zig index de9d6617..49a5daca 100644 --- a/src/browser/page.zig +++ b/src/browser/page.zig @@ -1132,6 +1132,11 @@ pub const Page = struct { } self.slot_change_monitor = try SlotChangeMonitor.init(self); } + + pub fn isSameOrigin(self: *const Page, url: []const u8) !bool { + const current_origin = try self.origin(self.call_arena); + return std.mem.startsWith(u8, url, current_origin); + } }; pub const NavigateReason = enum { diff --git a/src/tests/html/history.html b/src/tests/html/history.html index 3714d5cd..2c5591a4 100644 --- a/src/tests/html/history.html +++ b/src/tests/html/history.html @@ -19,8 +19,18 @@ let state = { "new": "field", testComplete: true }; testing.expectEqual(state, history.state); - testing.expectEqual(undefined, history.back()); - testing.expectEqual(undefined, history.forward()); + let popstateEventFired = false; + let popstateEventState = null; + + window.addEventListener('popstate', (event) => { + popstateEventFired = true; + popstateEventState = event.state; + }); + + testing.eventually(() => { + testing.expectEqual(true, popstateEventFired); + testing.expectEqual(state, popstateEventState); + }) testing.onPageWait(() => { testing.expectEqual(true, history.state && history.state.testComplete);