mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-14 15:28:57 +00:00
complete History impl backed by Navigation
This commit is contained in:
@@ -34,7 +34,6 @@ const Mime = @import("Mime.zig");
|
||||
const Factory = @import("Factory.zig");
|
||||
const Session = @import("Session.zig");
|
||||
const Scheduler = @import("Scheduler.zig");
|
||||
const History = @import("webapi/History.zig");
|
||||
const EventManager = @import("EventManager.zig");
|
||||
const ScriptManager = @import("ScriptManager.zig");
|
||||
|
||||
@@ -211,7 +210,6 @@ fn reset(self: *Page, comptime initializing: bool) !void {
|
||||
self.window = try self._factory.eventTarget(Window{
|
||||
._document = self.document,
|
||||
._storage_bucket = storage_bucket,
|
||||
._history = History.init(self),
|
||||
._performance = Performance.init(),
|
||||
._proto = undefined,
|
||||
._location = &default_location,
|
||||
@@ -1903,6 +1901,12 @@ const IdleNotification = union(enum) {
|
||||
}
|
||||
};
|
||||
|
||||
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
|
||||
const URLRaw = @import("URL.zig");
|
||||
const current_origin = (try URLRaw.getOrigin(self.arena, self.url)) orelse return false;
|
||||
return std.mem.startsWith(u8, url, current_origin);
|
||||
}
|
||||
|
||||
pub const NavigateReason = enum {
|
||||
anchor,
|
||||
address_bar,
|
||||
|
||||
@@ -23,6 +23,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 History = @import("webapi/History.zig");
|
||||
|
||||
const Page = @import("Page.zig");
|
||||
const Browser = @import("Browser.zig");
|
||||
@@ -55,6 +56,7 @@ executor: js.ExecutionWorld,
|
||||
cookie_jar: storage.Cookie.Jar,
|
||||
storage_shed: storage.Shed,
|
||||
|
||||
history: History,
|
||||
navigation: Navigation,
|
||||
|
||||
page: ?*Page = null,
|
||||
@@ -80,6 +82,7 @@ pub fn init(self: *Session, browser: *Browser) !void {
|
||||
.arena = session_allocator,
|
||||
.cookie_jar = storage.Cookie.Jar.init(allocator),
|
||||
.navigation = Navigation.init(session_allocator),
|
||||
.history = .{},
|
||||
.transfer_arena = browser.transfer_arena.allocator(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -569,6 +569,7 @@ pub const JsApis = flattenTypes(&.{
|
||||
@import("../webapi/event/ProgressEvent.zig"),
|
||||
@import("../webapi/event/NavigationCurrentEntryChangeEvent.zig"),
|
||||
@import("../webapi/event/PageTransitionEvent.zig"),
|
||||
@import("../webapi/event/PopStateEvent.zig"),
|
||||
@import("../webapi/MessageChannel.zig"),
|
||||
@import("../webapi/MessagePort.zig"),
|
||||
@import("../webapi/media/MediaError.zig"),
|
||||
|
||||
@@ -58,6 +58,7 @@ pub const Type = union(enum) {
|
||||
composition_event: *@import("event/CompositionEvent.zig"),
|
||||
navigation_current_entry_change_event: *@import("event/NavigationCurrentEntryChangeEvent.zig"),
|
||||
page_transition_event: *@import("event/PageTransitionEvent.zig"),
|
||||
pop_state_event: *@import("event/PopStateEvent.zig"),
|
||||
};
|
||||
|
||||
const Options = struct {
|
||||
|
||||
@@ -20,67 +20,75 @@ const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const PopStateEvent = @import("event/PopStateEvent.zig");
|
||||
|
||||
const History = @This();
|
||||
|
||||
_page: *Page,
|
||||
_length: u32 = 1,
|
||||
_state: ?js.Object = null,
|
||||
|
||||
pub fn init(page: *Page) History {
|
||||
return .{
|
||||
._page = page,
|
||||
};
|
||||
pub fn getLength(_: *const History, page: *Page) u32 {
|
||||
return @intCast(page._session.navigation._entries.items.len);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *History) void {
|
||||
if (self._state) |state| {
|
||||
js.q.JS_FreeValue(self._page.js.ctx, state.value);
|
||||
pub fn getState(_: *const History, page: *Page) !?js.Value {
|
||||
if (page._session.navigation.getCurrentEntry()._state.value) |state| {
|
||||
const value = try js.Value.fromJson(page.js, state);
|
||||
return value;
|
||||
} else return null;
|
||||
}
|
||||
|
||||
pub fn pushState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void {
|
||||
const arena = page._session.arena;
|
||||
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);
|
||||
|
||||
const json = state.toJson(arena) catch return error.DateClone;
|
||||
_ = try page._session.navigation.pushEntry(url, .{ .source = .history, .value = json }, page, true);
|
||||
}
|
||||
|
||||
pub fn replaceState(_: *History, state: js.Object, _: []const u8, _url: ?[]const u8, page: *Page) !void {
|
||||
const arena = page._session.arena;
|
||||
const url = if (_url) |u| try arena.dupeZ(u8, u) else try arena.dupeZ(u8, page.url);
|
||||
|
||||
const json = state.toJson(arena) catch return error.DateClone;
|
||||
_ = try page._session.navigation.replaceEntry(url, .{ .source = .history, .value = json }, page, true);
|
||||
}
|
||||
|
||||
fn goInner(delta: i32, page: *Page) !void {
|
||||
// 0 behaves the same as no argument, both reloadig the page.
|
||||
|
||||
const current = page._session.navigation._index;
|
||||
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
|
||||
if (index_s < 0 or index_s > page._session.navigation._entries.items.len - 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = @as(usize, @intCast(index_s));
|
||||
const entry = page._session.navigation._entries.items[index];
|
||||
|
||||
if (entry._url) |url| {
|
||||
if (try page.isSameOrigin(url)) {
|
||||
const event = try PopStateEvent.init("popstate", .{ .state = entry._state.value }, page);
|
||||
|
||||
try page._event_manager.dispatchWithFunction(
|
||||
page.window.asEventTarget(),
|
||||
event.asEvent(),
|
||||
page.window._on_popstate,
|
||||
.{ .context = "Pop State" },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_ = try page._session.navigation.navigateInner(entry._url, .{ .traverse = index }, page);
|
||||
}
|
||||
|
||||
pub fn getLength(self: *const History) u32 {
|
||||
return self._length;
|
||||
pub fn back(_: *History, page: *Page) !void {
|
||||
try goInner(-1, page);
|
||||
}
|
||||
|
||||
pub fn getState(self: *const History) ?js.Object {
|
||||
return self._state;
|
||||
pub fn forward(_: *History, page: *Page) !void {
|
||||
try goInner(1, page);
|
||||
}
|
||||
|
||||
pub fn pushState(self: *History, state: js.Object, _title: []const u8, url: ?[]const u8, page: *Page) !void {
|
||||
_ = _title; // title is ignored in modern browsers
|
||||
_ = url; // For minimal implementation, we don't actually navigate
|
||||
_ = page;
|
||||
|
||||
self._state = try state.persist();
|
||||
self._length += 1;
|
||||
}
|
||||
|
||||
pub fn replaceState(self: *History, state: js.Object, _title: []const u8, url: ?[]const u8, page: *Page) !void {
|
||||
_ = _title;
|
||||
_ = url;
|
||||
_ = page;
|
||||
self._state = try state.persist();
|
||||
// Note: replaceState doesn't change length
|
||||
}
|
||||
|
||||
pub fn back(self: *History, page: *Page) void {
|
||||
_ = self;
|
||||
_ = page;
|
||||
// Minimal implementation: no-op
|
||||
}
|
||||
|
||||
pub fn forward(self: *History, page: *Page) void {
|
||||
_ = self;
|
||||
_ = page;
|
||||
// Minimal implementation: no-op
|
||||
}
|
||||
|
||||
pub fn go(self: *History, delta: i32, page: *Page) void {
|
||||
_ = self;
|
||||
_ = delta;
|
||||
_ = page;
|
||||
// Minimal implementation: no-op
|
||||
pub fn go(_: *History, delta: ?i32, page: *Page) !void {
|
||||
try goInner(delta orelse 0, page);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
|
||||
@@ -52,10 +52,10 @@ _console: Console = .init,
|
||||
_navigator: Navigator = .init,
|
||||
_screen: Screen = .init,
|
||||
_performance: Performance,
|
||||
_history: History,
|
||||
_storage_bucket: *storage.Bucket,
|
||||
_on_load: ?js.Function = null,
|
||||
_on_pageshow: ?js.Function = null,
|
||||
_on_popstate: ?js.Function = null,
|
||||
_on_error: ?js.Function = null, // TODO: invoke on error?
|
||||
_on_unhandled_rejection: ?js.Function = null, // TODO: invoke on error
|
||||
_location: *Location,
|
||||
@@ -115,8 +115,8 @@ pub fn getLocation(self: *const Window) *Location {
|
||||
return self._location;
|
||||
}
|
||||
|
||||
pub fn getHistory(self: *Window) *History {
|
||||
return &self._history;
|
||||
pub fn getHistory(_: *Window, page: *Page) *History {
|
||||
return &page._session.history;
|
||||
}
|
||||
|
||||
pub fn getNavigation(_: *Window, page: *Page) *Navigation {
|
||||
@@ -151,6 +151,18 @@ pub fn setOnPageShow(self: *Window, cb_: ?js.Function) !void {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getOnPopState(self: *const Window) ?js.Function {
|
||||
return self._on_popstate;
|
||||
}
|
||||
|
||||
pub fn setOnPopState(self: *Window, cb_: ?js.Function) !void {
|
||||
if (cb_) |cb| {
|
||||
self._on_popstate = cb;
|
||||
} else {
|
||||
self._on_popstate = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getOnError(self: *const Window) ?js.Function {
|
||||
return self._on_error;
|
||||
}
|
||||
@@ -504,13 +516,14 @@ pub const JsApi = struct {
|
||||
pub const sessionStorage = bridge.accessor(Window.getSessionStorage, null, .{ .cache = "sessionStorage" });
|
||||
pub const document = bridge.accessor(Window.getDocument, null, .{ .cache = "document" });
|
||||
pub const location = bridge.accessor(Window.getLocation, null, .{ .cache = "location" });
|
||||
pub const history = bridge.accessor(Window.getHistory, null, .{ .cache = "history" });
|
||||
pub const history = bridge.accessor(Window.getHistory, null, .{});
|
||||
pub const navigation = bridge.accessor(Window.getNavigation, null, .{});
|
||||
pub const crypto = bridge.accessor(Window.getCrypto, null, .{ .cache = "crypto" });
|
||||
pub const CSS = bridge.accessor(Window.getCSS, null, .{ .cache = "CSS" });
|
||||
pub const customElements = bridge.accessor(Window.getCustomElements, null, .{ .cache = "customElements" });
|
||||
pub const onload = bridge.accessor(Window.getOnLoad, Window.setOnLoad, .{});
|
||||
pub const onpageshow = bridge.accessor(Window.getOnPageShow, Window.setOnPageShow, .{});
|
||||
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
|
||||
pub const onerror = bridge.accessor(Window.getOnError, Window.getOnError, .{});
|
||||
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
|
||||
pub const fetch = bridge.function(Window.fetch, .{});
|
||||
|
||||
72
src/browser/webapi/event/PopStateEvent.zig
Normal file
72
src/browser/webapi/event/PopStateEvent.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 log = @import("../../../log.zig");
|
||||
// const Window = @import("../html/window.zig").Window;
|
||||
const Event = @import("../Event.zig");
|
||||
const js = @import("../../js/js.zig");
|
||||
const Page = @import("../../Page.zig");
|
||||
|
||||
// https://developer.mozilla.org/en-US/docs/Web/API/PopStateEvent
|
||||
const PopStateEvent = @This();
|
||||
|
||||
const EventOptions = struct {
|
||||
state: ?[]const u8 = null,
|
||||
};
|
||||
|
||||
_proto: *Event,
|
||||
_state: ?[]const u8,
|
||||
|
||||
pub fn init(typ: []const u8, _options: ?EventOptions, page: *Page) !*PopStateEvent {
|
||||
const options = _options orelse EventOptions{};
|
||||
|
||||
return page._factory.event(typ, PopStateEvent{
|
||||
._proto = undefined,
|
||||
._state = options.state,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn asEvent(self: *PopStateEvent) *Event {
|
||||
return self._proto;
|
||||
}
|
||||
|
||||
pub fn getState(self: *PopStateEvent, page: *Page) !?js.Value {
|
||||
if (self._state == null) return null;
|
||||
|
||||
const value = try js.Value.fromJson(page.js, self._state.?);
|
||||
return value;
|
||||
}
|
||||
|
||||
pub fn getUAVisualTransition(_: *PopStateEvent) bool {
|
||||
// Not currently supported so we always return false;
|
||||
return false;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(PopStateEvent);
|
||||
|
||||
pub const Meta = struct {
|
||||
pub const name = "PopStateEvent";
|
||||
pub const prototype_chain = bridge.prototypeChain();
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(PopStateEvent.init, .{});
|
||||
pub const state = bridge.accessor(PopStateEvent.getState, null, .{});
|
||||
pub const hasUAVisualTransition = bridge.accessor(PopStateEvent.getUAVisualTransition, null, .{});
|
||||
};
|
||||
Reference in New Issue
Block a user