diff --git a/src/browser/Page.zig b/src/browser/Page.zig index ce3620fa..75b64413 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -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, diff --git a/src/browser/Session.zig b/src/browser/Session.zig index d8b4f0d1..5f25ae85 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -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(), }; } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index 5fccd42c..3358cc53 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -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"), diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 11da60fd..0a49655b 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.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 { diff --git a/src/browser/webapi/History.zig b/src/browser/webapi/History.zig index d80fe3ba..214f7230 100644 --- a/src/browser/webapi/History.zig +++ b/src/browser/webapi/History.zig @@ -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 { diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index d6b83bb4..72db1265 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -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, .{}); diff --git a/src/browser/webapi/event/PopStateEvent.zig b/src/browser/webapi/event/PopStateEvent.zig new file mode 100644 index 00000000..f6d7ce0f --- /dev/null +++ b/src/browser/webapi/event/PopStateEvent.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 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, .{}); +};