mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-14 15:28:57 +00:00
initial Navigation
This commit is contained in:
@@ -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);
|
||||
},
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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", .{});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
});
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, .{});
|
||||
};
|
||||
403
src/browser/webapi/navigation/Navigation.zig
Normal file
403
src/browser/webapi/navigation/Navigation.zig
Normal file
@@ -0,0 +1,403 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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, .{});
|
||||
};
|
||||
62
src/browser/webapi/navigation/NavigationEventTarget.zig
Normal file
62
src/browser/webapi/navigation/NavigationEventTarget.zig
Normal file
@@ -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,
|
||||
.{},
|
||||
);
|
||||
};
|
||||
88
src/browser/webapi/navigation/NavigationHistoryEntry.zig
Normal file
88
src/browser/webapi/navigation/NavigationHistoryEntry.zig
Normal file
@@ -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, .{});
|
||||
};
|
||||
72
src/browser/webapi/navigation/root.zig
Normal file
72
src/browser/webapi/navigation/root.zig
Normal file
@@ -0,0 +1,72 @@
|
||||
// Copyright (C) 2023-2024 Lightpanda (Selecy SAS)
|
||||
//
|
||||
// Francis Bouvier <francis@lightpanda.io>
|
||||
// Pierre Tachoire <pierre@lightpanda.io>
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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,
|
||||
};
|
||||
@@ -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", .{
|
||||
|
||||
Reference in New Issue
Block a user