add popstate event for History

This commit is contained in:
Muki Kiboigo
2025-09-24 00:20:34 -07:00
parent 464f42a121
commit e74d7fa454
6 changed files with 98 additions and 8 deletions

View File

@@ -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)).* },
};
}

View File

@@ -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");

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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");

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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);