add direct event handlers

This commit is contained in:
Muki Kiboigo
2025-10-13 16:17:48 -07:00
parent 4c957041e2
commit 82a45253de
9 changed files with 95 additions and 33 deletions

View File

@@ -17,6 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const js = @import("../js/js.zig");
const Allocator = std.mem.Allocator;
const log = @import("../../log.zig");
@@ -231,8 +232,6 @@ pub const EventHandler = struct {
node: parser.EventNode,
listener: *parser.EventListener,
const js = @import("../js/js.zig");
pub const Listener = union(enum) {
function: js.Function,
object: js.Object,
@@ -404,6 +403,40 @@ const SignalCallback = struct {
}
};
pub fn DirectEventHandler(
comptime TargetT: type,
target: *TargetT,
event_type: []const u8,
maybe_listener: ?EventHandler.Listener,
cb: *?js.Function,
page_arena: std.mem.Allocator,
) !void {
const event_target = parser.toEventTarget(TargetT, target);
// Check if we have a listener set.
if (cb.*) |callback| {
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
std.debug.assert(listener != null);
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
}
if (maybe_listener) |listener| {
switch (listener) {
// If an object is given as listener, do nothing.
.object => {},
.function => |callback| {
_ = try EventHandler.register(page_arena, event_target, event_type, listener, null) orelse unreachable;
cb.* = callback;
return;
},
}
}
// Just unset the listener.
cb.* = null;
}
const testing = @import("../../testing.zig");
test "Browser: Event" {
try testing.htmlRunner("events/event.html");

View File

@@ -177,5 +177,6 @@ pub const PopStateEvent = struct {
const testing = @import("../../testing.zig");
test "Browser: HTML.History" {
try testing.htmlRunner("html/history.html");
try testing.htmlRunner("html/history/history.html");
try testing.htmlRunner("html/history/history2.html");
}

View File

@@ -23,6 +23,7 @@ const URL = @import("../../url.zig").URL;
const js = @import("../js/js.zig");
const Page = @import("../page.zig").Page;
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
const EventHandler = @import("../events/event.zig").EventHandler;
@@ -62,6 +63,8 @@ index: usize = 0,
entries: std.ArrayListUnmanaged(*NavigationHistoryEntry) = .empty,
next_entry_id: usize = 0,
oncurrententrychange_callback: ?js.Function = null,
// https://developer.mozilla.org/en-US/docs/Web/API/NavigationHistoryEntry
const NavigationHistoryEntry = struct {
pub const prototype = *EventTarget;
@@ -199,6 +202,16 @@ pub fn _forward(self: *Navigation, page: *Page) !NavigationReturn {
return self.navigate(next_entry.url, .{ .traverse = new_index }, page);
}
/// Returns `oncurrententrychange_callback`.
pub fn get_oncurrententrychange(self: *const Navigation) ?js.Function {
return self.oncurrententrychange_callback;
}
/// Sets `oncurrententrychange_callback`.
pub fn set_oncurrententrychange(self: *Navigation, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
try DirectEventHandler(Navigation, self, "currententrychange", maybe_listener, &self.oncurrententrychange_callback, page.arena);
}
// 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 {
@@ -456,5 +469,5 @@ pub const NavigationCurrentEntryChangeEvent = struct {
const testing = @import("../../testing.zig");
test "Browser: Navigation" {
try testing.htmlRunner("html/navigation.html");
try testing.htmlRunner("html/navigation/navigation.html");
}

View File

@@ -44,6 +44,8 @@ const fetchFn = @import("../fetch/fetch.zig").fetch;
const storage = @import("../storage/storage.zig");
const ErrorEvent = @import("error_event.zig").ErrorEvent;
const DirectEventHandler = @import("../events/event.zig").DirectEventHandler;
// https://dom.spec.whatwg.org/#interface-window-extensions
// https://html.spec.whatwg.org/multipage/nav-history-apis.html#window
pub const Window = struct {
@@ -70,6 +72,7 @@ pub const Window = struct {
scroll_x: u32 = 0,
scroll_y: u32 = 0,
onload_callback: ?js.Function = null,
onpopstate_callback: ?js.Function = null,
pub fn create(target: ?[]const u8, navigator: ?Navigator) !Window {
var fbs = std.io.fixedBufferStream("");
@@ -111,31 +114,17 @@ pub const Window = struct {
/// Sets `onload_callback`.
pub fn set_onload(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
const event_target = parser.toEventTarget(Window, self);
const event_type = "load";
try DirectEventHandler(Window, self, "load", maybe_listener, &self.onload_callback, page.arena);
}
// Check if we have a listener set.
if (self.onload_callback) |callback| {
const listener = try parser.eventTargetHasListener(event_target, event_type, false, callback.id);
std.debug.assert(listener != null);
try parser.eventTargetRemoveEventListener(event_target, event_type, listener.?, false);
}
/// Returns `onpopstate_callback`.
pub fn get_onpopstate(self: *const Window) ?js.Function {
return self.onpopstate_callback;
}
if (maybe_listener) |listener| {
switch (listener) {
// If an object is given as listener, do nothing.
.object => {},
.function => |callback| {
_ = try EventHandler.register(page.arena, event_target, event_type, listener, null) orelse unreachable;
self.onload_callback = callback;
return;
},
}
}
// Just unset the listener.
self.onload_callback = null;
/// Sets `onpopstate_callback`.
pub fn set_onpopstate(self: *Window, maybe_listener: ?EventHandler.Listener, page: *Page) !void {
try DirectEventHandler(Window, self, "popstate", maybe_listener, &self.onpopstate_callback, page.arena);
}
pub fn get_location(self: *Window) *Location {

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script src="../../testing.js"></script>
<script id=history>
testing.expectEqual('auto', history.scrollRestoration);
@@ -11,7 +11,7 @@
testing.expectEqual('auto', history.scrollRestoration);
testing.expectEqual(null, history.state)
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/src/tests/html/history2.html');
history.pushState({ testInProgress: true }, null, 'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html');
testing.expectEqual({ testInProgress: true }, history.state);
history.pushState({ testInProgress: false }, null, 'http://127.0.0.1:9582/xhr/json');

View File

@@ -0,0 +1,26 @@
<!DOCTYPE html>
<script src="../../testing.js"></script>
<script id=history2>
history.pushState(
{"new": "field", testComplete: true },
null,
'http://127.0.0.1:9582/src/tests/html/history/history_after_nav.html'
);
let popstateEventFired = false;
let popstateEventState = null;
// uses the window event listener.
window.onpopstate = (event) => {
popstateEventFired = true;
popstateEventState = event.state;
};
testing.eventually(() => {
testing.expectEqual(true, popstateEventFired);
testing.expectEqual(true, popstateEventState.testComplete);
})
history.back();
</script>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script src="../../testing.js"></script>
<script id=history2>
testing.expectEqual(true, history.state && history.state.testInProgress);

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script src="../../testing.js"></script>
<script id=navigation>
testing.expectEqual('object', typeof navigation);
@@ -12,7 +12,7 @@
const currentIndex = navigation.currentEntry.index;
navigation.navigate(
'http://127.0.0.1:9582/src/tests/html/navigation2.html',
'http://127.0.0.1:9582/src/tests/html/navigation/navigation2.html',
{ state: { currentIndex: currentIndex, navTestInProgress: true } }
);
</script>

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script src="../../testing.js"></script>
<script id=navigation2>
const state = navigation.currentEntry.getState();