Skip to content

Commit d0b83c6

Browse files
Merge pull request #1138 from lightpanda-io/navigation
add `Navigation` WebAPI
2 parents b58ff2c + 28ec8d4 commit d0b83c6

25 files changed

+936
-171
lines changed

src/browser/dom/event_target.zig

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ pub const Union = union(enum) {
3434
screen_orientation: *@import("../html/screen.zig").ScreenOrientation,
3535
performance: *@import("performance.zig").Performance,
3636
media_query_list: *@import("../html/media_query_list.zig").MediaQueryList,
37+
navigation: *@import("../navigation/Navigation.zig"),
3738
};
3839

3940
// EventTarget implementation
@@ -82,6 +83,11 @@ pub const EventTarget = struct {
8283
.media_query_list => {
8384
return .{ .media_query_list = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et))) };
8485
},
86+
.navigation => {
87+
const NavigationEventTarget = @import("../navigation/NavigationEventTarget.zig");
88+
const base: *NavigationEventTarget = @fieldParentPtr("base", @as(*parser.EventTargetTBase, @ptrCast(et)));
89+
return .{ .navigation = @fieldParentPtr("proto", base) };
90+
},
8591
}
8692
}
8793

src/browser/events/event.zig

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
// along with this program. If not, see <https://www.gnu.org/licenses/>.
1818

1919
const std = @import("std");
20+
const js = @import("../js/js.zig");
2021
const Allocator = std.mem.Allocator;
2122

2223
const log = @import("../../log.zig");
@@ -38,6 +39,7 @@ const ErrorEvent = @import("../html/error_event.zig").ErrorEvent;
3839
const MessageEvent = @import("../dom/MessageChannel.zig").MessageEvent;
3940
const PopStateEvent = @import("../html/History.zig").PopStateEvent;
4041
const CompositionEvent = @import("composition_event.zig").CompositionEvent;
42+
const NavigationCurrentEntryChangeEvent = @import("../navigation/navigation.zig").NavigationCurrentEntryChangeEvent;
4143

4244
// Event interfaces
4345
pub const Interfaces = .{
@@ -50,6 +52,7 @@ pub const Interfaces = .{
5052
MessageEvent,
5153
PopStateEvent,
5254
CompositionEvent,
55+
NavigationCurrentEntryChangeEvent,
5356
};
5457

5558
pub const Union = generate.Union(Interfaces);
@@ -79,6 +82,9 @@ pub const Event = struct {
7982
.keyboard_event => .{ .KeyboardEvent = @as(*parser.KeyboardEvent, @ptrCast(evt)) },
8083
.pop_state => .{ .PopStateEvent = @as(*PopStateEvent, @ptrCast(evt)).* },
8184
.composition_event => .{ .CompositionEvent = (@as(*CompositionEvent, @fieldParentPtr("proto", evt))).* },
85+
.navigation_current_entry_change_event => .{
86+
.NavigationCurrentEntryChangeEvent = @as(*NavigationCurrentEntryChangeEvent, @ptrCast(evt)).*,
87+
},
8288
};
8389
}
8490

@@ -226,8 +232,6 @@ pub const EventHandler = struct {
226232
node: parser.EventNode,
227233
listener: *parser.EventListener,
228234

229-
const js = @import("../js/js.zig");
230-
231235
pub const Listener = union(enum) {
232236
function: js.Function,
233237
object: js.Object,
@@ -399,6 +403,40 @@ const SignalCallback = struct {
399403
}
400404
};
401405

406+
pub fn DirectEventHandler(
407+
comptime TargetT: type,
408+
target: *TargetT,
409+
event_type: []const u8,
410+
maybe_listener: ?EventHandler.Listener,
411+
cb: *?js.Function,
412+
page_arena: std.mem.Allocator,
413+
) !void {
414+
const event_target = parser.toEventTarget(TargetT, target);
415+
416+
// Check if we have a listener set.
417+
if (cb.*) |callback| {
418+
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
419+
std.debug.assert(listener != null);
420+
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
421+
}
422+
423+
if (maybe_listener) |listener| {
424+
switch (listener) {
425+
// If an object is given as listener, do nothing.
426+
.object => {},
427+
.function => |callback| {
428+
_ = try EventHandler.register(page_arena, event_target, event_type, listener, null) orelse unreachable;
429+
cb.* = callback;
430+
431+
return;
432+
},
433+
}
434+
}
435+
436+
// Just unset the listener.
437+
cb.* = null;
438+
}
439+
402440
const testing = @import("../../testing.zig");
403441
test "Browser: Event" {
404442
try testing.htmlRunner("events/event.html");

src/browser/html/History.zig

Lines changed: 58 additions & 90 deletions
Original file line numberDiff line numberDiff line change
@@ -21,140 +21,79 @@ const log = @import("../../log.zig");
2121

2222
const js = @import("../js/js.zig");
2323
const Page = @import("../page.zig").Page;
24+
const Window = @import("window.zig").Window;
2425

2526
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#the-history-interface
2627
const History = @This();
2728

28-
const HistoryEntry = struct {
29-
url: []const u8,
30-
// This is serialized as JSON because
31-
// History must survive a JsContext.
32-
state: ?[]u8,
33-
};
34-
3529
const ScrollRestorationMode = enum {
30+
pub const ENUM_JS_USE_TAG = true;
31+
3632
auto,
3733
manual,
38-
39-
pub fn fromString(str: []const u8) ?ScrollRestorationMode {
40-
for (std.enums.values(ScrollRestorationMode)) |mode| {
41-
if (std.ascii.eqlIgnoreCase(str, @tagName(mode))) {
42-
return mode;
43-
}
44-
} else {
45-
return null;
46-
}
47-
}
48-
49-
pub fn toString(self: ScrollRestorationMode) []const u8 {
50-
return @tagName(self);
51-
}
5234
};
5335

5436
scroll_restoration: ScrollRestorationMode = .auto,
55-
stack: std.ArrayListUnmanaged(HistoryEntry) = .empty,
56-
current: ?usize = null,
5737

58-
pub fn get_length(self: *History) u32 {
59-
return @intCast(self.stack.items.len);
38+
pub fn get_length(_: *History, page: *Page) u32 {
39+
return @intCast(page.session.navigation.entries.items.len);
6040
}
6141

6242
pub fn get_scrollRestoration(self: *History) ScrollRestorationMode {
6343
return self.scroll_restoration;
6444
}
6545

66-
pub fn set_scrollRestoration(self: *History, mode: []const u8) void {
67-
self.scroll_restoration = ScrollRestorationMode.fromString(mode) orelse self.scroll_restoration;
46+
pub fn set_scrollRestoration(self: *History, mode: ScrollRestorationMode) void {
47+
self.scroll_restoration = mode;
6848
}
6949

70-
pub fn get_state(self: *History, page: *Page) !?js.Value {
71-
if (self.current) |curr| {
72-
const entry = self.stack.items[curr];
73-
if (entry.state) |state| {
74-
const value = try js.Value.fromJson(page.js, state);
75-
return value;
76-
} else {
77-
return null;
78-
}
50+
pub fn get_state(_: *History, page: *Page) !?js.Value {
51+
if (page.session.navigation.currentEntry().state) |state| {
52+
const value = try js.Value.fromJson(page.js, state);
53+
return value;
7954
} else {
8055
return null;
8156
}
8257
}
8358

84-
pub fn pushNavigation(self: *History, _url: []const u8, page: *Page) !void {
59+
pub fn _pushState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
8560
const arena = page.session.arena;
86-
const url = try arena.dupe(u8, _url);
87-
88-
const entry = HistoryEntry{ .state = null, .url = url };
89-
try self.stack.append(arena, entry);
90-
self.current = self.stack.items.len - 1;
91-
}
92-
93-
pub fn dispatchPopStateEvent(state: ?[]const u8, page: *Page) void {
94-
log.debug(.script_event, "dispatch popstate event", .{
95-
.type = "popstate",
96-
.source = "history",
97-
});
98-
History._dispatchPopStateEvent(state, page) catch |err| {
99-
log.err(.app, "dispatch popstate event error", .{
100-
.err = err,
101-
.type = "popstate",
102-
.source = "history",
103-
});
104-
};
105-
}
106-
107-
fn _dispatchPopStateEvent(state: ?[]const u8, page: *Page) !void {
108-
var evt = try PopStateEvent.constructor("popstate", .{ .state = state });
61+
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
10962

110-
_ = try parser.eventTargetDispatchEvent(
111-
@as(*parser.EventTarget, @ptrCast(&page.window)),
112-
&evt.proto,
113-
);
63+
const json = state.toJson(arena) catch return error.DataClone;
64+
_ = try page.session.navigation.pushEntry(url, json, page, true);
11465
}
11566

116-
pub fn _pushState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
67+
pub fn _replaceState(_: *const History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
11768
const arena = page.session.arena;
11869

70+
const entry = page.session.navigation.currentEntry();
11971
const json = try state.toJson(arena);
12072
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
121-
const entry = HistoryEntry{ .state = json, .url = url };
122-
try self.stack.append(arena, entry);
123-
self.current = self.stack.items.len - 1;
124-
}
12573

126-
pub fn _replaceState(self: *History, state: js.Object, _: ?[]const u8, _url: ?[]const u8, page: *Page) !void {
127-
const arena = page.session.arena;
128-
129-
if (self.current) |curr| {
130-
const entry = &self.stack.items[curr];
131-
const json = try state.toJson(arena);
132-
const url = if (_url) |u| try arena.dupe(u8, u) else try arena.dupe(u8, page.url.raw);
133-
entry.* = HistoryEntry{ .state = json, .url = url };
134-
} else {
135-
try self._pushState(state, "", _url, page);
136-
}
74+
entry.state = json;
75+
entry.url = url;
13776
}
13877

139-
pub fn go(self: *History, delta: i32, page: *Page) !void {
78+
pub fn go(_: *const History, delta: i32, page: *Page) !void {
14079
// 0 behaves the same as no argument, both reloading the page.
141-
// If this is getting called, there SHOULD be an entry, atleast from pushNavigation.
142-
const current = self.current.?;
14380

81+
const current = page.session.navigation.index;
14482
const index_s: i64 = @intCast(@as(i64, @intCast(current)) + @as(i64, @intCast(delta)));
145-
if (index_s < 0 or index_s > self.stack.items.len - 1) {
83+
if (index_s < 0 or index_s > page.session.navigation.entries.items.len - 1) {
14684
return;
14785
}
14886

14987
const index = @as(usize, @intCast(index_s));
150-
const entry = self.stack.items[index];
151-
self.current = index;
88+
const entry = page.session.navigation.entries.items[index];
15289

153-
if (try page.isSameOrigin(entry.url)) {
154-
History.dispatchPopStateEvent(entry.state, page);
90+
if (entry.url) |url| {
91+
if (try page.isSameOrigin(url)) {
92+
PopStateEvent.dispatch(entry.state, page);
93+
}
15594
}
15695

157-
try page.navigateFromWebAPI(entry.url, .{ .reason = .history });
96+
_ = try page.session.navigation.navigate(entry.url, .{ .traverse = index }, page);
15897
}
15998

16099
pub fn _go(self: *History, _delta: ?i32, page: *Page) !void {
@@ -207,9 +146,38 @@ pub const PopStateEvent = struct {
207146
return null;
208147
}
209148
}
149+
150+
pub fn dispatch(state: ?[]const u8, page: *Page) void {
151+
log.debug(.script_event, "dispatch popstate event", .{
152+
.type = "popstate",
153+
.source = "history",
154+
});
155+
156+
var evt = PopStateEvent.constructor("popstate", .{ .state = state }) catch |err| {
157+
log.err(.app, "event constructor error", .{
158+
.err = err,
159+
.type = "popstate",
160+
.source = "history",
161+
});
162+
163+
return;
164+
};
165+
166+
_ = parser.eventTargetDispatchEvent(
167+
parser.toEventTarget(Window, &page.window),
168+
&evt.proto,
169+
) catch |err| {
170+
log.err(.app, "dispatch popstate event error", .{
171+
.err = err,
172+
.type = "popstate",
173+
.source = "history",
174+
});
175+
};
176+
}
210177
};
211178

212179
const testing = @import("../../testing.zig");
213180
test "Browser: HTML.History" {
214-
try testing.htmlRunner("html/history.html");
181+
try testing.htmlRunner("html/history/history.html");
182+
try testing.htmlRunner("html/history/history2.html");
215183
}

src/browser/html/document.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -195,7 +195,7 @@ pub const HTMLDocument = struct {
195195
}
196196

197197
pub fn set_location(_: *const parser.DocumentHTML, url: []const u8, page: *Page) !void {
198-
return page.navigateFromWebAPI(url, .{ .reason = .script });
198+
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
199199
}
200200

201201
pub fn get_designMode(_: *parser.DocumentHTML) []const u8 {

src/browser/html/location.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ pub const Location = struct {
3939
}
4040

4141
pub fn set_href(_: *const Location, href: []const u8, page: *Page) !void {
42-
return page.navigateFromWebAPI(href, .{ .reason = .script });
42+
return page.navigateFromWebAPI(href, .{ .reason = .script }, .{ .push = null });
4343
}
4444

4545
pub fn get_protocol(self: *Location) []const u8 {
@@ -75,15 +75,15 @@ pub const Location = struct {
7575
}
7676

7777
pub fn _assign(_: *const Location, url: []const u8, page: *Page) !void {
78-
return page.navigateFromWebAPI(url, .{ .reason = .script });
78+
return page.navigateFromWebAPI(url, .{ .reason = .script }, .{ .push = null });
7979
}
8080

8181
pub fn _replace(_: *const Location, url: []const u8, page: *Page) !void {
82-
return page.navigateFromWebAPI(url, .{ .reason = .script });
82+
return page.navigateFromWebAPI(url, .{ .reason = .script }, .replace);
8383
}
8484

8585
pub fn _reload(_: *const Location, page: *Page) !void {
86-
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script });
86+
return page.navigateFromWebAPI(page.url.raw, .{ .reason = .script }, .reload);
8787
}
8888

8989
pub fn _toString(self: *Location, page: *Page) ![]const u8 {

0 commit comments

Comments
 (0)