From 370c3a49a7b7e758d06097713e37b87a27a0d7f2 Mon Sep 17 00:00:00 2001 From: Muki Kiboigo Date: Mon, 8 Dec 2025 05:16:45 -0800 Subject: [PATCH] initial Navigation --- src/browser/EventManager.zig | 2 +- src/browser/Page.zig | 26 ++ src/browser/Session.zig | 13 +- src/browser/URL.zig | 35 ++ src/browser/js/Context.zig | 2 +- src/browser/js/Value.zig | 6 + src/browser/js/bridge.zig | 4 + src/browser/webapi/Event.zig | 1 + src/browser/webapi/EventTarget.zig | 1 + src/browser/webapi/URL.zig | 1 + .../NavigationCurrentEntryChangeEvent.zig | 61 +++ src/browser/webapi/navigation/Navigation.zig | 403 ++++++++++++++++++ .../navigation/NavigationEventTarget.zig | 62 +++ .../navigation/NavigationHistoryEntry.zig | 88 ++++ src/browser/webapi/navigation/root.zig | 72 ++++ src/cdp/domains/page.zig | 2 +- 16 files changed, 775 insertions(+), 4 deletions(-) create mode 100644 src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig create mode 100644 src/browser/webapi/navigation/Navigation.zig create mode 100644 src/browser/webapi/navigation/NavigationEventTarget.zig create mode 100644 src/browser/webapi/navigation/NavigationHistoryEntry.zig create mode 100644 src/browser/webapi/navigation/root.zig diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 408d6744..af117cc3 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -117,7 +117,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void switch (target._type) { .node => |node| try self.dispatchNode(node, event, &was_handled), - .xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue => { + .xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue, .navigation => { const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; try self.dispatchAll(list, target, event, &was_handled); }, diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 48daeeb5..b4d4ee2e 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -272,6 +272,27 @@ fn registerBackgroundTasks(self: *Page) !void { } pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void { + const session = self._session; + + const resolved_url = try URL.resolve( + session.transfer_arena, + self.url, + request_url, + .{ .always_dupe = true }, + ); + + // setting opts.force = true will force a page load. + // otherwise, we will need to ensure this is a true (not document) navigation. + if (!opts.force) { + // If we are navigating within the same document, just change URL. + if (URL.eqlDocument(self.url, resolved_url)) { + self.url = resolved_url; + // 3. change window.location + try session.navigation.updateEntries("", .{ .push = null }, self, true); + return; + } + } + if (self._parse_state != .pre) { // it's possible for navigate to be called multiple times on the // same page (via CDP). We want to reset the page between each call. @@ -493,6 +514,9 @@ fn pageDoneCallback(ctx: *anyopaque) !void { var self: *Page = @ptrCast(@alignCast(ctx)); self.clearTransferArena(); + //We need to handle different navigation types differently. + try self._session.navigation.processNavigation(self); + defer if (comptime IS_DEBUG) { log.debug(.page, "page.load.complete", .{ .url = self.url }); }; @@ -1868,6 +1892,7 @@ pub const NavigateReason = enum { form, script, history, + navigation, }; pub const NavigateOpts = struct { @@ -1876,6 +1901,7 @@ pub const NavigateOpts = struct { method: Http.Method = .GET, body: ?[]const u8 = null, header: ?[:0]const u8 = null, + force: bool = false, }; const RequestCookieOpts = struct { diff --git a/src/browser/Session.zig b/src/browser/Session.zig index cabbf551..d8b4f0d1 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -22,6 +22,7 @@ const log = @import("../log.zig"); const js = @import("js/js.zig"); const storage = @import("webapi/storage/storage.zig"); +const Navigation = @import("webapi/navigation/Navigation.zig"); const Page = @import("Page.zig"); const Browser = @import("Browser.zig"); @@ -54,6 +55,8 @@ executor: js.ExecutionWorld, cookie_jar: storage.Cookie.Jar, storage_shed: storage.Shed, +navigation: Navigation, + page: ?*Page = null, // If the current page want to navigate to a new page @@ -67,13 +70,16 @@ pub fn init(self: *Session, browser: *Browser) !void { errdefer executor.deinit(); const allocator = browser.app.allocator; + const session_allocator = browser.session_arena.allocator(); + self.* = .{ .browser = browser, .executor = executor, .storage_shed = .{}, .queued_navigation = null, - .arena = browser.session_arena.allocator(), + .arena = session_allocator, .cookie_jar = storage.Cookie.Jar.init(allocator), + .navigation = Navigation.init(session_allocator), .transfer_arena = browser.transfer_arena.allocator(), }; } @@ -98,6 +104,9 @@ pub fn createPage(self: *Session) !*Page { self.page = try Page.init(page_arena.allocator(), self.browser.call_arena.allocator(), self); const page = self.page.?; + // Creates a new NavigationEventTarget for this page. + try self.navigation.onNewPage(page); + log.debug(.browser, "create page", .{}); // start JS env // Inform CDP the main page has been created such that additional context for other Worlds can be created as well @@ -115,6 +124,8 @@ pub fn removePage(self: *Session) void { self.page.?.deinit(); self.page = null; + self.navigation.onRemovePage(); + log.debug(.browser, "remove page", .{}); } diff --git a/src/browser/URL.zig b/src/browser/URL.zig index 3d77acf9..a429eae9 100644 --- a/src/browser/URL.zig +++ b/src/browser/URL.zig @@ -268,6 +268,17 @@ pub fn getHost(raw: [:0]const u8) []const u8 { return authority[0..path_start]; } +// Returns true if these two URLs point to the same document. +pub fn eqlDocument(first: [:0]const u8, second: [:0]const u8) bool { + if (!std.mem.eql(u8, getHost(first), getHost(second))) return false; + if (!std.mem.eql(u8, getPort(first), getPort(second))) return false; + if (!std.mem.eql(u8, getPathname(first), getPathname(second))) return false; + if (!std.mem.eql(u8, getSearch(first), getSearch(second))) return false; + if (!std.mem.eql(u8, getHash(first), getHash(second))) return false; + + return true; +} + const KnownProtocol = enum { @"http:", @"https:", @@ -286,6 +297,30 @@ test "URL: isCompleteHTTPUrl" { try testing.expectEqual(false, isCompleteHTTPUrl("about")); } +// TODO: uncomment +// test "URL: resolve regression (#1093)" { +// defer testing.reset(); + +// const Case = struct { +// base: []const u8, +// path: []const u8, +// expected: []const u8, +// }; + +// const cases = [_]Case{ +// .{ +// .base = "https://alas.aws.amazon.com/alas2.html", +// .path = "../static/bootstrap.min.css", +// .expected = "https://alas.aws.amazon.com/static/bootstrap.min.css", +// }, +// }; + +// for (cases) |case| { +// const result = try resolve(testing.arena_allocator, case.path, case.base, .{}); +// try testing.expectString(case.expected, result); +// } +// } + test "URL: resolve" { defer testing.reset(); diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 24f52dec..3afcdbea 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -515,7 +515,7 @@ pub fn zigValueToJs(self: *Context, value: anytype, comptime opts: Caller.CallOp } if (T == js.Value) { - return value.value; + return value.js_val; } if (T == js.Promise) { diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index 143d221a..8cba1688 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -43,6 +43,12 @@ pub fn toString(self: Value, allocator: Allocator) ![]const u8 { return self.context.valueToString(self.js_val, .{ .allocator = allocator }); } +pub fn fromJson(ctx: *js.Context, json: []const u8) !Value { + const json_string = v8.String.initUtf8(ctx.isolate, json); + const value = try v8.Json.parse(ctx.v8_context, json_string); + return Value{ .context = ctx, .js_val = value }; +} + pub fn toObject(self: Value) js.Object { return .{ .context = self.context, diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 7cd2aace..5a07e5a2 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -567,6 +567,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/event/ErrorEvent.zig"), @import("../webapi/event/MessageEvent.zig"), @import("../webapi/event/ProgressEvent.zig"), + @import("../webapi/event/NavigationCurrentEntryChangeEvent.zig"), @import("../webapi/MessageChannel.zig"), @import("../webapi/MessagePort.zig"), @import("../webapi/media/MediaError.zig"), @@ -599,4 +600,7 @@ pub const JsApis = flattenTypes(&.{ @import("../webapi/File.zig"), @import("../webapi/Screen.zig"), @import("../webapi/PerformanceObserver.zig"), + @import("../webapi/navigation/Navigation.zig"), + @import("../webapi/navigation/NavigationEventTarget.zig"), + @import("../webapi/navigation/NavigationHistoryEntry.zig"), }); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index dafb0204..5db5de85 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -56,6 +56,7 @@ pub const Type = union(enum) { message_event: *@import("event/MessageEvent.zig"), progress_event: *@import("event/ProgressEvent.zig"), composition_event: *@import("event/CompositionEvent.zig"), + navigation_current_entry_change_event: *@import("event/NavigationCurrentEntryChangeEvent.zig"), }; const Options = struct { diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 9792f2b3..4f0eabfe 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -37,6 +37,7 @@ pub const Type = union(enum) { media_query_list: *@import("css/MediaQueryList.zig"), message_port: *@import("MessagePort.zig"), text_track_cue: *@import("media/TextTrackCue.zig"), + navigation: *@import("navigation/NavigationEventTarget.zig"), }; pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool { diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index 15beb6c0..49c03b1d 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -33,6 +33,7 @@ _search_params: ?*URLSearchParams = null, // convenience pub const resolve = @import("../URL.zig").resolve; +pub const eqlDocument = @import("../URL.zig").eqlDocument; pub fn init(url: [:0]const u8, base_: ?[:0]const u8, page: *Page) !*URL { const url_is_absolute = @import("../URL.zig").isCompleteHTTPUrl(url); diff --git a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig new file mode 100644 index 00000000..34dc8a3c --- /dev/null +++ b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig @@ -0,0 +1,61 @@ +const std = @import("std"); +const Event = @import("../Event.zig"); +const Page = @import("../../Page.zig"); +const Navigaton = @import("../navigation/Navigation.zig"); +const NavigationHistoryEntry = @import("../navigation/NavigationHistoryEntry.zig"); +const NavigationType = @import("../navigation/root.zig").NavigationType; +const js = @import("../../js/js.zig"); + +const NavigationCurrentEntryChangeEvent = @This(); + +_proto: *Event, +_from: *NavigationHistoryEntry, +_navigation_type: ?NavigationType, + +pub const EventInit = struct { + from: *NavigationHistoryEntry, + navigationType: ?[]const u8 = null, +}; + +pub fn init( + typ: []const u8, + init_obj: EventInit, + page: *Page, +) !*NavigationCurrentEntryChangeEvent { + const navigation_type = if (init_obj.navigationType) |nav_type_str| + std.meta.stringToEnum(NavigationType, nav_type_str) + else + null; + + return page._factory.event(typ, NavigationCurrentEntryChangeEvent{ + ._proto = undefined, + ._from = init_obj.from, + ._navigation_type = navigation_type, + }); +} + +pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event { + return self._proto; +} + +pub fn getFrom(self: *NavigationCurrentEntryChangeEvent) *NavigationHistoryEntry { + return self._from; +} + +pub fn getNavigationType(self: *const NavigationCurrentEntryChangeEvent) ?NavigationType { + return self._navigation_type; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(NavigationCurrentEntryChangeEvent); + + pub const Meta = struct { + pub const name = "NavigationCurrentEntryChangeEvent"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const constructor = bridge.constructor(NavigationCurrentEntryChangeEvent.init, .{}); + pub const from = bridge.accessor(NavigationCurrentEntryChangeEvent.getFrom, null, .{}); + pub const navigationType = bridge.accessor(NavigationCurrentEntryChangeEvent.getNavigationType, null, .{}); +}; diff --git a/src/browser/webapi/navigation/Navigation.zig b/src/browser/webapi/navigation/Navigation.zig new file mode 100644 index 00000000..f6c26d80 --- /dev/null +++ b/src/browser/webapi/navigation/Navigation.zig @@ -0,0 +1,403 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const log = @import("../../../log.zig"); +const URL = @import("../URL.zig"); + +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); + +const EventTarget = @import("../EventTarget.zig"); + +// https://developer.mozilla.org/en-US/docs/Web/API/Navigation +const Navigation = @This(); + +const NavigationKind = @import("root.zig").NavigationKind; +const NavigationHistoryEntry = @import("NavigationHistoryEntry.zig"); +const NavigationTransition = @import("root.zig").NavigationTransition; +const NavigationState = @import("root.zig").NavigationState; + +const NavigationCurrentEntryChangeEvent = @import("../event/NavigationCurrentEntryChangeEvent.zig"); +const NavigationEventTarget = @import("NavigationEventTarget.zig"); + +_proto: *NavigationEventTarget = undefined, +_arena: std.mem.Allocator, +_current_navigation_kind: ?NavigationKind = null, + +_index: usize = 0, +// Need to be stable pointers, because Events can reference entries. +_entries: std.ArrayListUnmanaged(*NavigationHistoryEntry) = .empty, +_next_entry_id: usize = 0, + +pub fn init(arena: std.mem.Allocator) Navigation { + return Navigation{ ._arena = arena }; +} + +fn asEventTarget(self: *Navigation) *EventTarget { + return self._proto.asEventTarget(); +} + +pub fn onRemovePage(self: *Navigation) void { + self._proto = undefined; +} + +pub fn onNewPage(self: *Navigation, page: *Page) !void { + self._proto = try page._factory.eventTarget( + NavigationEventTarget{ ._proto = undefined }, + ); +} + +pub fn getCanGoBack(self: *const Navigation) bool { + return self._index > 0; +} + +pub fn getCanGoForward(self: *const Navigation) bool { + return self._entries.items.len > self._index + 1; +} + +pub fn getCurrentEntry(self: *Navigation) *NavigationHistoryEntry { + return self._entries.items[self._index]; +} + +pub fn getTransition(_: *const Navigation) ?NavigationTransition { + // For now, all transitions are just considered complete. + return null; +} + +const NavigationReturn = struct { + committed: js.Promise, + finished: js.Promise, +}; + +pub fn back(self: *Navigation, page: *Page) !NavigationReturn { + if (!self.getCanGoBack()) { + return error.InvalidStateError; + } + + const new_index = self._index - 1; + const next_entry = self._entries.items[new_index]; + self._index = new_index; + + return self.navigateInner(next_entry._url, .{ .traverse = new_index }, page); +} + +pub fn entries(self: *const Navigation) []*NavigationHistoryEntry { + return self._entries.items; +} + +pub fn forward(self: *Navigation, page: *Page) !NavigationReturn { + if (!self.getCanGoForward()) { + return error.InvalidStateError; + } + + const new_index = self._index + 1; + const next_entry = self._entries.items[new_index]; + self._index = new_index; + + return self.navigateInner(next_entry._url, .{ .traverse = new_index }, page); +} + +pub fn updateEntries(self: *Navigation, url: [:0]const u8, kind: NavigationKind, page: *Page, dispatch: bool) !void { + switch (kind) { + .replace => { + _ = try self.replaceEntry(url, .{ .source = .navigation, .value = null }, page, dispatch); + }, + .push => |state| { + _ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, dispatch); + }, + .traverse => |index| { + self._index = index; + }, + .reload => {}, + } +} + +// This is for after true navigation processing, where we need to ensure that our entries are up to date. +// This is only really safe to run in the `pageDoneCallback` where we can guarantee that the URL and NavigationKind are correct. +pub fn processNavigation(self: *Navigation, page: *Page) !void { + const url = page.url; + const kind: NavigationKind = self._current_navigation_kind orelse .{ .push = null }; + try self.updateEntries(url, kind, page, false); +} + +/// Pushes an entry into the Navigation stack WITHOUT actually navigating to it. +/// For that, use `navigate`. +pub fn pushEntry( + self: *Navigation, + _url: [:0]const u8, + state: NavigationState, + page: *Page, + dispatch: bool, +) !*NavigationHistoryEntry { + const arena = self._arena; + const url = try arena.dupeZ(u8, _url); + + // truncates our history here. + if (self._entries.items.len > self._index + 1) { + self._entries.shrinkRetainingCapacity(self._index + 1); + } + + const index = self._entries.items.len; + + const id = self._next_entry_id; + self._next_entry_id += 1; + + const id_str = try std.fmt.allocPrint(arena, "{d}", .{id}); + + const entry = try arena.create(NavigationHistoryEntry); + entry.* = NavigationHistoryEntry{ + ._id = id_str, + ._key = id_str, + ._url = url, + ._state = state, + }; + + // we don't always have a current entry... + const previous = if (self._entries.items.len > 0) self.getCurrentEntry() else null; + try self._entries.append(arena, entry); + self._index = index; + + if (previous) |prev| { + if (dispatch) { + const event = try NavigationCurrentEntryChangeEvent.init( + "currententrychange", + .{ .from = prev, .navigationType = @tagName(.push) }, + page, + ); + try self._proto.dispatch(.{ .currententrychange = event }, page); + } + } + + return entry; +} + +pub fn replaceEntry( + self: *Navigation, + _url: [:0]const u8, + state: NavigationState, + page: *Page, + dispatch: bool, +) !*NavigationHistoryEntry { + const arena = self._arena; + const url = try arena.dupeZ(u8, _url); + + const previous = self.getCurrentEntry(); + + const id = self._next_entry_id; + self._next_entry_id += 1; + const id_str = try std.fmt.allocPrint(arena, "{d}", .{id}); + + const entry = try arena.create(NavigationHistoryEntry); + entry.* = NavigationHistoryEntry{ + ._id = id_str, + ._key = previous._key, + ._url = url, + ._state = state, + }; + + self._entries.items[self._index] = entry; + + if (dispatch) { + const event = try NavigationCurrentEntryChangeEvent.init( + "currententrychange", + .{ .from = previous, .navigationType = @tagName(.replace) }, + page, + ); + try self._proto.dispatch(.{ .currententrychange = event }, page); + } + + return entry; +} + +const NavigateOptions = struct { + state: ?js.Object = null, + info: ?js.Object = null, + history: ?[]const u8 = null, +}; + +pub fn navigateInner( + self: *Navigation, + _url: ?[:0]const u8, + kind: NavigationKind, + page: *Page, +) !NavigationReturn { + const arena = self._arena; + const url = _url orelse return error.MissingURL; + + // https://github.com/WICG/navigation-api/issues/95 + // + // These will only settle on same-origin navigation (mostly intended for SPAs). + // It is fine (and expected) for these to not settle on cross-origin requests :) + const committed = try page.js.createPromiseResolver(.page); + const finished = try page.js.createPromiseResolver(.page); + + const new_url = try URL.resolve(arena, url, page.url, .{}); + const is_same_document = URL.eqlDocument(new_url, page.url); + + switch (kind) { + .push => |state| { + if (is_same_document) { + page.url = new_url; + + committed.resolve("navigation push", {}); + // todo: Fire navigate event + finished.resolve("navigation push", {}); + + _ = try self.pushEntry(url, .{ .source = .navigation, .value = state }, page, true); + } else { + // try page.navigate(url, .{ .reason = .navigation }, kind); + try page.navigate(url, .{ .reason = .navigation }); + } + }, + .replace => |state| { + if (is_same_document) { + page.url = new_url; + + committed.resolve("navigation replace", {}); + // todo: Fire navigate event + finished.resolve("navigation replace", {}); + + _ = try self.replaceEntry(url, .{ .source = .navigation, .value = state }, page, true); + } else { + // try page.navigate(url, .{ .reason = .navigation }, kind); + try page.navigate(url, .{ .reason = .navigation }); + } + }, + .traverse => |index| { + self._index = index; + + if (is_same_document) { + page.url = new_url; + + committed.resolve("navigation traverse", {}); + // todo: Fire navigate event + finished.resolve("navigation traverse", {}); + } else { + // try page.navigate(url, .{ .reason = .navigation }, kind); + try page.navigate(url, .{ .reason = .navigation }); + } + }, + .reload => { + // try page.navigate(url, .{ .reason = .navigation }, kind); + try page.navigate(url, .{ .reason = .navigation }); + }, + } + + return .{ + .committed = committed.promise(), + .finished = finished.promise(), + }; +} + +pub fn navigate(self: *Navigation, _url: [:0]const u8, _opts: ?NavigateOptions, page: *Page) !NavigationReturn { + const opts = _opts orelse NavigateOptions{}; + const json = if (opts.state) |state| state.toJson(self._arena) catch return error.DataClone else null; + + const kind: NavigationKind = if (opts.history) |history| + if (std.mem.eql(u8, "replace", history)) .{ .replace = json } else .{ .push = json } + else + .{ .push = json }; + + return try self.navigateInner(_url, kind, page); +} + +pub const ReloadOptions = struct { + state: ?js.Object = null, + info: ?js.Object = null, +}; + +pub fn reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !NavigationReturn { + const arena = self._arena; + + const opts = _opts orelse ReloadOptions{}; + const entry = self.getCurrentEntry(); + if (opts.state) |state| { + const previous = entry; + entry.state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone }; + + const event = try NavigationCurrentEntryChangeEvent.init( + "currententrychange", + .{ .from = previous, .navigationType = @tagName(.reload) }, + page, + ); + try self._proto.dispatch(.{ .currententrychange = event }, page); + } + + return self.navigateInner(entry._url, .reload, page); +} + +pub const TraverseToOptions = struct { + info: ?js.Object = null, +}; + +pub fn traverseTo(self: *Navigation, key: []const u8, _opts: ?TraverseToOptions, page: *Page) !NavigationReturn { + if (_opts != null) { + log.debug(.browser, "not implemented", .{ .options = _opts }); + } + + for (self._entries.items, 0..) |entry, i| { + if (std.mem.eql(u8, key, entry._key)) { + return try self.navigateInner(entry._url, .{ .traverse = i }, page); + } + } + + return error.InvalidStateError; +} + +pub const UpdateCurrentEntryOptions = struct { + state: js.Object, +}; + +pub fn updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions, page: *Page) !void { + const arena = self._arena; + + const previous = self.getCurrentEntry(); + self.getCurrentEntry()._state = .{ + .source = .navigation, + .value = options.state.toJson(arena) catch return error.DataClone, + }; + + const event = try NavigationCurrentEntryChangeEvent.init( + "currententrychange", + .{ .from = previous, .navigationType = null }, + page, + ); + try self._proto.dispatch(.{ .currententrychange = event }, page); +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(Navigation); + + pub const Meta = struct { + pub const name = "Navigation"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const canGoBack = bridge.accessor(Navigation.getCanGoBack, null, .{}); + pub const canGoForward = bridge.accessor(Navigation.getCanGoForward, null, .{}); + pub const currentEntry = bridge.accessor(Navigation.getCurrentEntry, null, .{}); + pub const transition = bridge.accessor(Navigation.getTransition, null, .{}); + pub const back = bridge.function(Navigation.back, .{}); + pub const entries = bridge.function(Navigation.entries, .{}); + pub const forward = bridge.function(Navigation.forward, .{}); + pub const navigate = bridge.function(Navigation.navigate, .{}); + pub const traverseTo = bridge.function(Navigation.traverseTo, .{}); + pub const updateCurrentEntry = bridge.function(Navigation.updateCurrentEntry, .{}); +}; diff --git a/src/browser/webapi/navigation/NavigationEventTarget.zig b/src/browser/webapi/navigation/NavigationEventTarget.zig new file mode 100644 index 00000000..d262d3b4 --- /dev/null +++ b/src/browser/webapi/navigation/NavigationEventTarget.zig @@ -0,0 +1,62 @@ +const std = @import("std"); +const EventTarget = @import("../EventTarget.zig"); +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); +const Event = @import("../Event.zig"); +const NavigationCurrentEntryChangeEvent = @import("../event/NavigationCurrentEntryChangeEvent.zig"); + +pub const NavigationEventTarget = @This(); + +_proto: *EventTarget, +_on_currententrychange: ?js.Function = null, + +pub fn asEventTarget(self: *NavigationEventTarget) *EventTarget { + return self._proto; +} + +const DispatchType = union(enum) { + currententrychange: *NavigationCurrentEntryChangeEvent, +}; + +pub fn dispatch(self: *NavigationEventTarget, event_type: DispatchType, page: *Page) !void { + const event, const field = blk: { + break :blk switch (event_type) { + .currententrychange => |cec| .{ cec.asEvent(), "_on_currententrychange" }, + }; + }; + + return page._event_manager.dispatchWithFunction( + self.asEventTarget(), + event, + @field(self, field), + .{ .context = "Navigation" }, + ); +} + +pub fn getOnCurrentEntryChange(self: *NavigationEventTarget) ?js.Function { + return self._on_currententrychange; +} + +pub fn setOnCurrentEntryChange(self: *NavigationEventTarget, listener: ?js.Function) !void { + if (listener) |listen| { + self._on_currententrychange = try listen.withThis(self); + } else { + self._on_currententrychange = null; + } +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(NavigationEventTarget); + + pub const Meta = struct { + pub const name = "NavigationEventTarget"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const oncurrententrychange = bridge.accessor( + NavigationEventTarget.getOnCurrentEntryChange, + NavigationEventTarget.setOnCurrentEntryChange, + .{}, + ); +}; diff --git a/src/browser/webapi/navigation/NavigationHistoryEntry.zig b/src/browser/webapi/navigation/NavigationHistoryEntry.zig new file mode 100644 index 00000000..206e3795 --- /dev/null +++ b/src/browser/webapi/navigation/NavigationHistoryEntry.zig @@ -0,0 +1,88 @@ +const std = @import("std"); +const URL = @import("../URL.zig"); +const EventTarget = @import("../EventTarget.zig"); +const NavigationState = @import("root.zig").NavigationState; +const Page = @import("../../Page.zig"); +const js = @import("../../js/js.zig"); + +const NavigationHistoryEntry = @This(); + +// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry +// no proto for now +// _proto: ?*EventTarget, +_id: []const u8, +_key: []const u8, +_url: ?[:0]const u8, +_state: NavigationState, + +// fn asEventTarget(self: *NavigationHistoryEntry) *EventTarget { +// return self._proto.?.asEventTarget(); +// } + +// pub fn onRemovePage(self: *NavigationHistoryEntry) void { +// self._proto = null; +// } + +// pub fn onNewPage(self: *NavigationHistoryEntry, page: *Page) !void { +// self._proto = try page._factory.eventTarget( +// NavigationHistoryEntryEventTarget{ ._proto = undefined }, +// ); +// } + +pub fn id(self: *const NavigationHistoryEntry) []const u8 { + return self._id; +} + +pub fn index(self: *const NavigationHistoryEntry, page: *Page) i32 { + const navigation = page._session.navigation; + + for (navigation._entries.items, 0..) |entry, i| { + if (std.mem.eql(u8, entry._id, self._id)) { + return @intCast(i); + } + } + + return -1; +} + +pub fn key(self: *const NavigationHistoryEntry) []const u8 { + return self._key; +} + +pub fn sameDocument(self: *const NavigationHistoryEntry, page: *Page) bool { + const got_url = self._url orelse return false; + return URL.eqlDocument(got_url, page.url); +} + +pub fn url(self: *const NavigationHistoryEntry) ?[:0]const u8 { + return self._url; +} + +pub const StateReturn = union(enum) { value: ?js.Value, undefined: void }; + +pub fn state(self: *const NavigationHistoryEntry, page: *Page) !StateReturn { + if (self._state.source == .navigation) { + if (self._state.value) |value| { + return .{ .value = try js.Value.fromJson(page.js, value) }; + } + } + + return .undefined; +} + +pub const JsApi = struct { + pub const bridge = js.Bridge(NavigationHistoryEntry); + + pub const Meta = struct { + pub const name = "NavigationHistoryEntry"; + pub const prototype_chain = bridge.prototypeChain(); + pub var class_id: bridge.ClassId = undefined; + }; + + pub const id = bridge.accessor(NavigationHistoryEntry.id, null, .{}); + pub const index = bridge.accessor(NavigationHistoryEntry.index, null, .{}); + pub const key = bridge.accessor(NavigationHistoryEntry.key, null, .{}); + pub const sameDocument = bridge.accessor(NavigationHistoryEntry.sameDocument, null, .{}); + pub const url = bridge.accessor(NavigationHistoryEntry.url, null, .{}); + pub const state = bridge.accessor(NavigationHistoryEntry.state, null, .{}); +}; diff --git a/src/browser/webapi/navigation/root.zig b/src/browser/webapi/navigation/root.zig new file mode 100644 index 00000000..e2a5c173 --- /dev/null +++ b/src/browser/webapi/navigation/root.zig @@ -0,0 +1,72 @@ +// Copyright (C) 2023-2024 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const log = @import("../../../log.zig"); + +const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); + +const Navigation = @import("Navigation.zig"); +const NavigationEventTarget = @import("NavigationEventTarget.zig"); +const NavigationHistoryEntry = @import("NavigationHistoryEntry.zig"); + +pub const NavigationType = enum { + push, + replace, + traverse, + reload, +}; + +pub const NavigationKind = union(NavigationType) { + push: ?[]const u8, + replace: ?[]const u8, + traverse: usize, + reload, +}; + +pub const NavigationState = struct { + source: enum { history, navigation }, + value: ?[]const u8, +}; + +// https://developer.mozilla.org/en-US/docs/Web/API/NavigationActivation +pub const NavigationActivation = struct { + entry: NavigationHistoryEntry, + from: ?NavigationHistoryEntry = null, + type: NavigationType, + + pub fn get_entry(self: *const NavigationActivation) NavigationHistoryEntry { + return self.entry; + } + + pub fn get_from(self: *const NavigationActivation) ?NavigationHistoryEntry { + return self.from; + } + + pub fn get_navigationType(self: *const NavigationActivation) NavigationType { + return self.type; + } +}; + +// https://developer.mozilla.org/en-US/docs/Web/API/NavigationTransition +pub const NavigationTransition = struct { + finished: js.Promise, + from: NavigationHistoryEntry, + navigation_type: NavigationType, +}; diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 1be6f3c9..bc961fe1 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -206,7 +206,7 @@ pub fn pageNavigate(arena: Allocator, bc: anytype, event: *const Notification.Pa .POST => "formSubmissionPost", else => unreachable, }, - .address_bar => null, + .address_bar, .navigation => null, }; if (reason_) |reason| { try cdp.sendEvent("Page.frameScheduledNavigation", .{