mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-28 22:53:28 +00:00
Merge pull request #516 from karlseguin/javascript_anchor_click
Some checks failed
e2e-test / zig build release (push) Has been cancelled
wpt / web platform tests (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
Some checks failed
e2e-test / zig build release (push) Has been cancelled
wpt / web platform tests (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
zig-test / zig build dev (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
e2e-test / puppeteer-perf (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
zig-test / browser fetch (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
nightly build / build-linux-x86_64 (push) Has been cancelled
nightly build / build-linux-aarch64 (push) Has been cancelled
nightly build / build-macos-aarch64 (push) Has been cancelled
nightly build / build-macos-x86_64 (push) Has been cancelled
Add zig listener support to netsurf event handler
This commit is contained in:
@@ -134,6 +134,10 @@ pub const Session = struct {
|
||||
storage_shed: storage.Shed,
|
||||
cookie_jar: storage.CookieJar,
|
||||
|
||||
// arbitrary that we pass to the inspector, which the inspector will include
|
||||
// in any response/event that it emits.
|
||||
aux_data: ?[]const u8 = null,
|
||||
|
||||
page: ?Page = null,
|
||||
http_client: *http.Client,
|
||||
|
||||
@@ -158,6 +162,7 @@ pub const Session = struct {
|
||||
const allocator = app.allocator;
|
||||
self.* = .{
|
||||
.app = app,
|
||||
.aux_data = null,
|
||||
.browser = browser,
|
||||
.notify_ctx = any_ctx,
|
||||
.inspector = undefined,
|
||||
@@ -250,8 +255,12 @@ pub const Session = struct {
|
||||
// load polyfills
|
||||
try polyfill.load(self.arena.allocator(), self.executor);
|
||||
|
||||
if (aux_data) |ad| {
|
||||
self.aux_data = try self.arena.allocator().dupe(u8, ad);
|
||||
}
|
||||
|
||||
// inspector
|
||||
self.contextCreated(page, aux_data);
|
||||
self.contextCreated(page);
|
||||
|
||||
return page;
|
||||
}
|
||||
@@ -279,9 +288,28 @@ pub const Session = struct {
|
||||
return &(self.page orelse return null);
|
||||
}
|
||||
|
||||
fn contextCreated(self: *Session, page: *Page, aux_data: ?[]const u8) void {
|
||||
fn pageNavigate(self: *Session, url_string: []const u8) !void {
|
||||
// currently, this is only called from the page, so let's hope
|
||||
// it isn't null!
|
||||
std.debug.assert(self.page != null);
|
||||
|
||||
// can't use the page arena, because we're about to reset it
|
||||
// and don't want to use the session's arena, because that'll start to
|
||||
// look like a leak if we navigate from page to page a lot.
|
||||
var buf: [1024]u8 = undefined;
|
||||
var fba = std.heap.FixedBufferAllocator.init(&buf);
|
||||
const url = try self.page.?.url.?.resolve(fba.allocator(), url_string);
|
||||
|
||||
self.removePage();
|
||||
var page = try self.createPage(null);
|
||||
return page.navigate(url, .{
|
||||
.reason = .anchor,
|
||||
});
|
||||
}
|
||||
|
||||
fn contextCreated(self: *Session, page: *Page) void {
|
||||
log.debug("inspector context created", .{});
|
||||
self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", aux_data);
|
||||
self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", self.aux_data);
|
||||
}
|
||||
|
||||
fn notify(self: *const Session, notification: *const Notification) void {
|
||||
@@ -361,7 +389,7 @@ pub const Page = struct {
|
||||
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
|
||||
// - aux_data: extra data forwarded to the Inspector
|
||||
// see Inspector.contextCreated
|
||||
pub fn navigate(self: *Page, request_url: URL, aux_data: ?[]const u8) !void {
|
||||
pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
|
||||
const arena = self.arena;
|
||||
const session = self.session;
|
||||
|
||||
@@ -387,7 +415,12 @@ pub const Page = struct {
|
||||
var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true });
|
||||
defer request.deinit();
|
||||
|
||||
session.notify(&.{ .page_navigate = .{ .url = url, .timestamp = timestamp() } });
|
||||
session.notify(&.{ .page_navigate = .{
|
||||
.url = url,
|
||||
.reason = opts.reason,
|
||||
.timestamp = timestamp(),
|
||||
} });
|
||||
|
||||
var response = try request.sendSync(.{});
|
||||
|
||||
// would be different than self.url in the case of a redirect
|
||||
@@ -417,7 +450,7 @@ pub const Page = struct {
|
||||
var mime = try Mime.parse(arena, ct);
|
||||
|
||||
if (mime.isHTML()) {
|
||||
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8", aux_data);
|
||||
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8");
|
||||
} else {
|
||||
log.info("non-HTML document: {s}", .{ct});
|
||||
var arr: std.ArrayListUnmanaged(u8) = .{};
|
||||
@@ -428,44 +461,14 @@ pub const Page = struct {
|
||||
self.raw_data = arr.items;
|
||||
}
|
||||
|
||||
session.notify(&.{ .page_navigated = .{ .url = url, .timestamp = timestamp() } });
|
||||
}
|
||||
|
||||
pub const ClickResult = union(enum) {
|
||||
navigate: std.Uri,
|
||||
};
|
||||
|
||||
pub const MouseEvent = struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
type: Type,
|
||||
|
||||
const Type = enum {
|
||||
pressed,
|
||||
released,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn mouseEvent(self: *Page, me: MouseEvent) !void {
|
||||
if (me.type != .pressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return;
|
||||
|
||||
const event = try parser.mouseEventCreate();
|
||||
defer parser.mouseEventDestroy(event);
|
||||
try parser.mouseEventInit(event, "click", .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.x = me.x,
|
||||
.y = me.y,
|
||||
});
|
||||
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
|
||||
session.notify(&.{ .page_navigated = .{
|
||||
.url = url,
|
||||
.timestamp = timestamp(),
|
||||
} });
|
||||
}
|
||||
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, aux_data: ?[]const u8) !void {
|
||||
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
|
||||
const arena = self.arena;
|
||||
|
||||
// start netsurf memory arena.
|
||||
@@ -481,6 +484,16 @@ pub const Page = struct {
|
||||
// save a document's pointer in the page.
|
||||
self.doc = doc;
|
||||
|
||||
const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
|
||||
try parser.eventTargetAddZigListener(
|
||||
parser.toEventTarget(parser.Element, document_element),
|
||||
arena,
|
||||
"click",
|
||||
windowClicked,
|
||||
self,
|
||||
false,
|
||||
);
|
||||
|
||||
// TODO set document.readyState to interactive
|
||||
// https://html.spec.whatwg.org/#reporting-document-loading-status
|
||||
|
||||
@@ -497,7 +510,7 @@ pub const Page = struct {
|
||||
// https://html.spec.whatwg.org/#read-html
|
||||
|
||||
// inspector
|
||||
session.contextCreated(self, aux_data);
|
||||
session.contextCreated(self);
|
||||
|
||||
{
|
||||
// update the sessions state
|
||||
@@ -728,6 +741,61 @@ pub const Page = struct {
|
||||
return request;
|
||||
}
|
||||
|
||||
pub const MouseEvent = struct {
|
||||
x: i32,
|
||||
y: i32,
|
||||
type: Type,
|
||||
|
||||
const Type = enum {
|
||||
pressed,
|
||||
released,
|
||||
};
|
||||
};
|
||||
|
||||
pub fn mouseEvent(self: *Page, me: MouseEvent) !void {
|
||||
if (me.type != .pressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return;
|
||||
|
||||
const event = try parser.mouseEventCreate();
|
||||
defer parser.mouseEventDestroy(event);
|
||||
try parser.mouseEventInit(event, "click", .{
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
.x = me.x,
|
||||
.y = me.y,
|
||||
});
|
||||
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
|
||||
}
|
||||
|
||||
fn windowClicked(ctx: *anyopaque, event: *parser.Event) void {
|
||||
const self: *Page = @alignCast(@ptrCast(ctx));
|
||||
self._windowClicked(event) catch |err| {
|
||||
log.err("window click handler: {}", .{err});
|
||||
};
|
||||
}
|
||||
|
||||
fn _windowClicked(self: *Page, event: *parser.Event) !void {
|
||||
const target = (try parser.eventTarget(event)) orelse return;
|
||||
|
||||
const node = parser.eventTargetToNode(target);
|
||||
if (try parser.nodeType(node) != .element) {
|
||||
return;
|
||||
}
|
||||
|
||||
const html_element: *parser.ElementHTML = @ptrCast(node);
|
||||
switch (try parser.elementHTMLGetTagType(html_element)) {
|
||||
.a => {
|
||||
const element: *parser.Element = @ptrCast(node);
|
||||
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
|
||||
return self.session.pageNavigate(href);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
const Script = struct {
|
||||
element: *parser.Element,
|
||||
kind: Kind,
|
||||
@@ -792,8 +860,17 @@ pub const Page = struct {
|
||||
};
|
||||
};
|
||||
|
||||
pub const NavigateReason = enum {
|
||||
anchor,
|
||||
address_bar,
|
||||
};
|
||||
|
||||
const NavigateOpts = struct {
|
||||
reason: NavigateReason = .address_bar,
|
||||
};
|
||||
|
||||
// provide very poor abstration to the rest of the code. In theory, we can change
|
||||
// the FlatRendere to a different implementation, and it'll all just work.
|
||||
// the FlatRenderer to a different implementation, and it'll all just work.
|
||||
pub const Renderer = FlatRenderer;
|
||||
|
||||
// This "renderer" positions elements in a single row in an unspecified order.
|
||||
|
||||
@@ -104,7 +104,7 @@ pub const MutationObserver = struct {
|
||||
arena,
|
||||
"DOMNodeInserted",
|
||||
EventHandler,
|
||||
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
|
||||
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
|
||||
false,
|
||||
);
|
||||
try parser.eventTargetAddEventListener(
|
||||
@@ -112,7 +112,7 @@ pub const MutationObserver = struct {
|
||||
arena,
|
||||
"DOMNodeRemoved",
|
||||
EventHandler,
|
||||
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
|
||||
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
|
||||
false,
|
||||
);
|
||||
}
|
||||
@@ -122,7 +122,7 @@ pub const MutationObserver = struct {
|
||||
arena,
|
||||
"DOMAttrModified",
|
||||
EventHandler,
|
||||
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
|
||||
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
|
||||
false,
|
||||
);
|
||||
}
|
||||
@@ -132,7 +132,7 @@ pub const MutationObserver = struct {
|
||||
arena,
|
||||
"DOMCharacterDataModified",
|
||||
EventHandler,
|
||||
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
|
||||
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
|
||||
false,
|
||||
);
|
||||
}
|
||||
@@ -142,7 +142,7 @@ pub const MutationObserver = struct {
|
||||
arena,
|
||||
"DOMSubtreeModified",
|
||||
EventHandler,
|
||||
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
|
||||
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
|
||||
false,
|
||||
);
|
||||
}
|
||||
@@ -261,7 +261,7 @@ const EventHandler = struct {
|
||||
return false;
|
||||
}
|
||||
|
||||
fn handle(evt: ?*parser.Event, data: parser.EventHandlerData) void {
|
||||
fn handle(evt: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
|
||||
if (evt == null) return;
|
||||
|
||||
var mrs: MutationRecords = .{};
|
||||
@@ -277,7 +277,7 @@ const EventHandler = struct {
|
||||
const node = parser.eventTargetToNode(et);
|
||||
|
||||
// retrieve the observer from the data.
|
||||
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.data));
|
||||
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.ctx));
|
||||
|
||||
if (!apply(o, node)) return;
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ pub const Event = struct {
|
||||
};
|
||||
|
||||
pub const EventHandler = struct {
|
||||
fn handle(event: ?*parser.Event, data: parser.EventHandlerData) void {
|
||||
fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
|
||||
var result: Callback.Result = undefined;
|
||||
data.cbk.tryCall(.{if (event) |evt| Event.toInterface(evt) catch unreachable else null}, &result) catch {
|
||||
log.err("event handler error: {s}", .{result.exception});
|
||||
|
||||
@@ -131,6 +131,18 @@ pub const HTMLElement = struct {
|
||||
// attach the text node.
|
||||
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(t)));
|
||||
}
|
||||
|
||||
pub fn _click(e: *parser.ElementHTML) !void {
|
||||
const event = try parser.mouseEventCreate();
|
||||
defer parser.mouseEventDestroy(event);
|
||||
try parser.mouseEventInit(event, "click", .{
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.bubbles = true,
|
||||
.cancelable = true,
|
||||
});
|
||||
_ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
|
||||
}
|
||||
};
|
||||
|
||||
// Deprecated HTMLElements in Chrome (2023/03/15)
|
||||
@@ -1050,4 +1062,12 @@ test "Browser.HTML.Element" {
|
||||
.{ "document.getElementById('content').innerText", "foo" },
|
||||
.{ "document.getElementById('content').innerHTML = backup; true;", "true" },
|
||||
}, .{});
|
||||
|
||||
try runner.testCases(&.{
|
||||
.{ "let click_count = 0;", "undefined" },
|
||||
.{ "let clickCbk = function() { click_count++ }", "undefined" },
|
||||
.{ "document.getElementById('content').addEventListener('click', clickCbk);", "undefined" },
|
||||
.{ "document.getElementById('content').click()", "undefined" },
|
||||
.{ "click_count", "1" },
|
||||
}, .{});
|
||||
}
|
||||
|
||||
@@ -616,10 +616,12 @@ pub fn eventTargetHasListener(
|
||||
// and capture property,
|
||||
// let's check if the callback handler is the same
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
const ehd = EventHandlerDataInternal.fromListener(listener);
|
||||
if (ehd) |d| {
|
||||
if (cbk_id == d.data.cbk.id) {
|
||||
return lst;
|
||||
if (EventHandlerData.fromListener(listener)) |ehd| {
|
||||
switch (ehd.*) {
|
||||
.js => |js| if (cbk_id == js.data.cbk.id) {
|
||||
return lst;
|
||||
},
|
||||
.zig => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -636,100 +638,115 @@ pub fn eventTargetHasListener(
|
||||
return null;
|
||||
}
|
||||
|
||||
// EventHandlerFunc is a zig function called when the event is dispatched to a
|
||||
// listener.
|
||||
// The EventHandlerFunc is responsible to call the callback included into the
|
||||
// EventHandlerData.
|
||||
pub const EventHandlerFunc = *const fn (event: ?*Event, data: EventHandlerData) void;
|
||||
// The *anyopque that get stored in the libdom listener, which we'll retrieve
|
||||
// when then event is dispatched so that we can execute the JS or Zig callback.
|
||||
const EventHandlerData = union(enum) {
|
||||
js: JS,
|
||||
zig: Zig,
|
||||
|
||||
// EventHandler implements the function exposed in C and called by libdom.
|
||||
// It retrieves the EventHandlerInternalData and call the EventHandlerFunc with
|
||||
// the EventHandlerData in parameter.
|
||||
const EventHandler = struct {
|
||||
fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void {
|
||||
if (data) |d| {
|
||||
const ehd = EventHandlerDataInternal.get(d);
|
||||
ehd.handler(event, ehd.data);
|
||||
const JS = struct {
|
||||
data: JSEventHandlerData,
|
||||
func: JSEventHandlerFunc,
|
||||
};
|
||||
|
||||
// NOTE: we can not call func.deinit here
|
||||
// b/c the handler can be called several times
|
||||
// either on this dispatch event or in anoter one
|
||||
}
|
||||
}
|
||||
}.handle;
|
||||
const Zig = struct {
|
||||
ctx: *anyopaque,
|
||||
func: ZigEventHandlerFunc,
|
||||
};
|
||||
|
||||
// EventHandlerData contains a JS callback and the data associated to the
|
||||
// handler.
|
||||
// If given, deinitFunc is called with the data pointer to allow the creator to
|
||||
// clean memory.
|
||||
// The callback is deinit by EventHandlerDataInternal. It must NOT be deinit
|
||||
// into deinitFunc.
|
||||
pub const EventHandlerData = struct {
|
||||
cbk: Callback,
|
||||
data: ?*anyopaque = null,
|
||||
// deinitFunc implements the data deinitialization.
|
||||
deinitFunc: ?DeinitFunc = null,
|
||||
|
||||
pub const DeinitFunc = *const fn (data: ?*anyopaque, allocator: std.mem.Allocator) void;
|
||||
};
|
||||
|
||||
// EventHandlerDataInternal groups the EventHandlerFunc and the EventHandlerData.
|
||||
const EventHandlerDataInternal = struct {
|
||||
data: EventHandlerData,
|
||||
handler: EventHandlerFunc,
|
||||
|
||||
fn init(alloc: std.mem.Allocator, handler: EventHandlerFunc, data: EventHandlerData) !*EventHandlerDataInternal {
|
||||
const ptr = try alloc.create(EventHandlerDataInternal);
|
||||
ptr.* = .{
|
||||
.data = data,
|
||||
.handler = handler,
|
||||
};
|
||||
return ptr;
|
||||
// retrieve a EventHandlerDataInternal from a listener.
|
||||
fn fromListener(lst: *EventListener) ?*EventHandlerData {
|
||||
const ctx = eventListenerGetData(lst) orelse return null;
|
||||
const ehd: *EventHandlerData = @alignCast(@ptrCast(ctx));
|
||||
return ehd;
|
||||
}
|
||||
|
||||
fn deinit(self: *EventHandlerDataInternal, alloc: std.mem.Allocator) void {
|
||||
if (self.data.deinitFunc) |d| {
|
||||
d(self.data.data, alloc);
|
||||
pub fn deinit(self: *EventHandlerData, alloc: std.mem.Allocator) void {
|
||||
switch (self.*) {
|
||||
.js => |*js| {
|
||||
const js_data = &js.data;
|
||||
if (js_data.deinitFunc) |df| {
|
||||
df(js_data.ctx, alloc);
|
||||
}
|
||||
},
|
||||
.zig => {},
|
||||
}
|
||||
alloc.destroy(self);
|
||||
}
|
||||
|
||||
fn get(data: *anyopaque) *EventHandlerDataInternal {
|
||||
const ptr: *align(@alignOf(*EventHandlerDataInternal)) anyopaque = @alignCast(data);
|
||||
return @as(*EventHandlerDataInternal, @ptrCast(ptr));
|
||||
}
|
||||
|
||||
// retrieve a EventHandlerDataInternal from a listener.
|
||||
fn fromListener(lst: *EventListener) ?*EventHandlerDataInternal {
|
||||
const data = eventListenerGetData(lst);
|
||||
// free cbk allocation made on eventTargetAddEventListener
|
||||
if (data == null) return null;
|
||||
|
||||
return get(data.?);
|
||||
pub fn handle(self: *EventHandlerData, event: ?*Event) void {
|
||||
switch (self.*) {
|
||||
.js => |*js| js.func(event, &js.data),
|
||||
.zig => |zig| zig.func(zig.ctx, event.?),
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub const JSEventHandlerData = struct {
|
||||
cbk: Callback,
|
||||
ctx: ?*anyopaque = null,
|
||||
// deinitFunc implements the data deinitialization.
|
||||
deinitFunc: ?DeinitFunc = null,
|
||||
|
||||
pub const DeinitFunc = *const fn (data: ?*anyopaque, alloc: std.mem.Allocator) void;
|
||||
};
|
||||
|
||||
const JSEventHandlerFunc = *const fn (event: ?*Event, data: *JSEventHandlerData) void;
|
||||
const ZigEventHandlerFunc = *const fn (ctx: *anyopaque, event: *Event) void;
|
||||
|
||||
pub fn eventTargetAddEventListener(
|
||||
et: *EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
typ: []const u8,
|
||||
handlerFunc: EventHandlerFunc,
|
||||
data: EventHandlerData,
|
||||
func: JSEventHandlerFunc,
|
||||
data: JSEventHandlerData,
|
||||
capture: bool,
|
||||
) !void {
|
||||
// this allocation will be removed either on
|
||||
// eventTargetRemoveEventListener or eventTargetRemoveAllEventListeners
|
||||
const ehd = try EventHandlerDataInternal.init(alloc, handlerFunc, data);
|
||||
const ehd = try alloc.create(EventHandlerData);
|
||||
errdefer alloc.destroy(ehd);
|
||||
ehd.* = .{ .js = .{ .data = data, .func = func } };
|
||||
errdefer ehd.deinit(alloc);
|
||||
|
||||
// When a function is used as an event handler, its this parameter is bound
|
||||
// to the DOM element on which the listener is placed.
|
||||
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/this#this_in_dom_event_handlers
|
||||
try ehd.data.cbk.setThis(et);
|
||||
try ehd.js.data.cbk.setThis(et);
|
||||
|
||||
return addEventTargetListener(et, typ, ehd, capture);
|
||||
}
|
||||
|
||||
pub fn eventTargetAddZigListener(
|
||||
et: *EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
typ: []const u8,
|
||||
func: ZigEventHandlerFunc,
|
||||
ctx: *anyopaque,
|
||||
capture: bool,
|
||||
) !void {
|
||||
const ehd = try alloc.create(EventHandlerData);
|
||||
errdefer alloc.destroy(ehd);
|
||||
ehd.* = .{ .zig = .{ .ctx = ctx, .func = func } };
|
||||
return addEventTargetListener(et, typ, ehd, capture);
|
||||
}
|
||||
|
||||
fn addEventTargetListener(et: *EventTarget, typ: []const u8, data: *anyopaque, capture: bool) !void {
|
||||
// event_handler implements the function exposed in C and called by libdom.
|
||||
// It retrieves the EventHandler and calls the appropriate (JS or Zig)
|
||||
// handler function with the corresponding data.
|
||||
const event_handler = struct {
|
||||
fn handle(event: ?*Event, ptr_: ?*anyopaque) callconv(.C) void {
|
||||
const ptr = ptr_ orelse return;
|
||||
@as(*EventHandlerData, @alignCast(@ptrCast(ptr))).handle(event);
|
||||
// NOTE: we can not call func.deinit here
|
||||
// b/c the handler can be called several times
|
||||
// either on this dispatch event or in anoter one
|
||||
}
|
||||
}.handle;
|
||||
|
||||
const ctx = @as(*anyopaque, @ptrCast(ehd));
|
||||
var listener: ?*EventListener = undefined;
|
||||
const errLst = c.dom_event_listener_create(EventHandler, ctx, &listener);
|
||||
const errLst = c.dom_event_listener_create(event_handler, data, &listener);
|
||||
try DOMErr(errLst);
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
|
||||
@@ -746,8 +763,9 @@ pub fn eventTargetRemoveEventListener(
|
||||
capture: bool,
|
||||
) !void {
|
||||
// free data allocation made on eventTargetAddEventListener
|
||||
const ehd = EventHandlerDataInternal.fromListener(lst);
|
||||
if (ehd) |d| d.deinit(alloc);
|
||||
if (EventHandlerData.fromListener(lst)) |ehd| {
|
||||
ehd.deinit(alloc);
|
||||
}
|
||||
|
||||
const s = try strFromData(typ);
|
||||
const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture);
|
||||
@@ -776,16 +794,21 @@ pub fn eventTargetRemoveAllEventListeners(
|
||||
if (lst) |listener| {
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
|
||||
const ehd = EventHandlerDataInternal.fromListener(listener);
|
||||
if (ehd) |d| d.deinit(alloc);
|
||||
if (EventHandlerData.fromListener(listener)) |ehd| {
|
||||
if (ehd.* == .zig) {
|
||||
// we don't remove Zig listeners
|
||||
continue;
|
||||
}
|
||||
|
||||
const err = eventTargetVtable(et).remove_event_listener.?(
|
||||
et,
|
||||
null,
|
||||
lst,
|
||||
false,
|
||||
);
|
||||
try DOMErr(err);
|
||||
ehd.deinit(alloc);
|
||||
const err = eventTargetVtable(et).remove_event_listener.?(
|
||||
et,
|
||||
null,
|
||||
lst,
|
||||
false,
|
||||
);
|
||||
try DOMErr(err);
|
||||
}
|
||||
}
|
||||
|
||||
if (next == null) {
|
||||
|
||||
@@ -151,13 +151,6 @@ fn navigate(cmd: anytype) !void {
|
||||
|
||||
const url = try URL.parse(params.url, "https");
|
||||
|
||||
const aux_data = try std.fmt.allocPrint(
|
||||
cmd.arena,
|
||||
// NOTE: we assume this is the default web page
|
||||
"{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\"}}",
|
||||
.{target_id},
|
||||
);
|
||||
|
||||
var page = bc.session.currentPage().?;
|
||||
bc.loader_id = bc.cdp.loader_id_gen.next();
|
||||
try cmd.sendResult(.{
|
||||
@@ -165,10 +158,13 @@ fn navigate(cmd: anytype) !void {
|
||||
.loaderId = bc.loader_id,
|
||||
}, .{});
|
||||
|
||||
try page.navigate(url, aux_data);
|
||||
std.debug.print("page: {s}\n", .{target_id});
|
||||
try page.navigate(url, .{
|
||||
.reason = .address_bar,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void {
|
||||
pub fn pageNavigate(bc: anytype, event: *const Notification.PageNavigate) !void {
|
||||
// I don't think it's possible that we get these notifications and don't
|
||||
// have these things setup.
|
||||
std.debug.assert(bc.session.page != null);
|
||||
@@ -180,6 +176,22 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void {
|
||||
|
||||
bc.reset();
|
||||
|
||||
if (event.reason == .anchor) {
|
||||
try cdp.sendEvent("Page.frameScheduledNavigation", .{
|
||||
.frameId = target_id,
|
||||
.delay = 0,
|
||||
.reason = "anchorClick",
|
||||
.url = event.url.raw,
|
||||
}, .{ .session_id = session_id });
|
||||
|
||||
try cdp.sendEvent("Page.frameRequestedNavigation", .{
|
||||
.frameId = target_id,
|
||||
.reason = "anchorClick",
|
||||
.url = event.url.raw,
|
||||
.disposition = "currentTab",
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
// frameStartedNavigating event
|
||||
try cdp.sendEvent("Page.frameStartedNavigating", .{
|
||||
.frameId = target_id,
|
||||
@@ -202,12 +214,18 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void {
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
if (event.reason == .anchor) {
|
||||
try cdp.sendEvent("Page.frameClearedScheduledNavigation", .{
|
||||
.frameId = target_id,
|
||||
}, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
// Send Runtime.executionContextsCleared event
|
||||
// TODO: noop event, we have no env context at this point, is it necesarry?
|
||||
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
|
||||
}
|
||||
|
||||
pub fn pageNavigated(bc: anytype, event: *const Notification.PageEvent) !void {
|
||||
pub fn pageNavigated(bc: anytype, event: *const Notification.PageNavigated) !void {
|
||||
// I don't think it's possible that we get these notifications and don't
|
||||
// have these things setup.
|
||||
std.debug.assert(bc.session.page != null);
|
||||
|
||||
@@ -161,9 +161,9 @@ const Page = struct {
|
||||
aux_data: []const u8 = "",
|
||||
doc: ?*parser.Document = null,
|
||||
|
||||
pub fn navigate(_: *Page, url: URL, aux_data: []const u8) !void {
|
||||
pub fn navigate(_: *Page, url: URL, opts: anytype) !void {
|
||||
_ = url;
|
||||
_ = aux_data;
|
||||
_ = opts;
|
||||
}
|
||||
|
||||
const MouseEvent = @import("../browser/browser.zig").Page.MouseEvent;
|
||||
|
||||
263
src/events/event.zig
Normal file
263
src/events/event.zig
Normal file
@@ -0,0 +1,263 @@
|
||||
// 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 generate = @import("../generate.zig");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
const CallbackResult = jsruntime.CallbackResult;
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("netsurf");
|
||||
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
||||
|
||||
const ProgressEvent = @import("../xhr/progress_event.zig").ProgressEvent;
|
||||
|
||||
const log = std.log.scoped(.events);
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = .{
|
||||
Event,
|
||||
ProgressEvent,
|
||||
};
|
||||
|
||||
pub const Union = generate.Union(Interfaces);
|
||||
|
||||
// https://dom.spec.whatwg.org/#event
|
||||
pub const Event = struct {
|
||||
pub const Self = parser.Event;
|
||||
pub const Exception = DOMException;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub const EventInit = parser.EventInit;
|
||||
|
||||
// JS
|
||||
// --
|
||||
|
||||
pub const _CAPTURING_PHASE = 1;
|
||||
pub const _AT_TARGET = 2;
|
||||
pub const _BUBBLING_PHASE = 3;
|
||||
|
||||
pub fn toInterface(evt: *parser.Event) !Union {
|
||||
return switch (try parser.eventGetInternalType(evt)) {
|
||||
.event => .{ .Event = evt },
|
||||
.progress_event => .{ .ProgressEvent = @as(*ProgressEvent, @ptrCast(evt)).* },
|
||||
};
|
||||
}
|
||||
|
||||
pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event {
|
||||
const event = try parser.eventCreate();
|
||||
try parser.eventInit(event, eventType, opts orelse EventInit{});
|
||||
return event;
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
pub fn get_type(self: *parser.Event) ![]const u8 {
|
||||
return try parser.eventType(self);
|
||||
}
|
||||
|
||||
pub fn get_target(self: *parser.Event) !?EventTargetUnion {
|
||||
const et = try parser.eventTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?);
|
||||
}
|
||||
|
||||
pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion {
|
||||
const et = try parser.eventCurrentTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?);
|
||||
}
|
||||
|
||||
pub fn get_eventPhase(self: *parser.Event) !u8 {
|
||||
return try parser.eventPhase(self);
|
||||
}
|
||||
|
||||
pub fn get_bubbles(self: *parser.Event) !bool {
|
||||
return try parser.eventBubbles(self);
|
||||
}
|
||||
|
||||
pub fn get_cancelable(self: *parser.Event) !bool {
|
||||
return try parser.eventCancelable(self);
|
||||
}
|
||||
|
||||
pub fn get_defaultPrevented(self: *parser.Event) !bool {
|
||||
return try parser.eventDefaultPrevented(self);
|
||||
}
|
||||
|
||||
pub fn get_isTrusted(self: *parser.Event) !bool {
|
||||
return try parser.eventIsTrusted(self);
|
||||
}
|
||||
|
||||
pub fn get_timestamp(self: *parser.Event) !u32 {
|
||||
return try parser.eventTimestamp(self);
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
pub fn _initEvent(
|
||||
self: *parser.Event,
|
||||
eventType: []const u8,
|
||||
bubbles: ?bool,
|
||||
cancelable: ?bool,
|
||||
) !void {
|
||||
const opts = EventInit{
|
||||
.bubbles = bubbles orelse false,
|
||||
.cancelable = cancelable orelse false,
|
||||
};
|
||||
return try parser.eventInit(self, eventType, opts);
|
||||
}
|
||||
|
||||
pub fn _stopPropagation(self: *parser.Event) !void {
|
||||
return try parser.eventStopPropagation(self);
|
||||
}
|
||||
|
||||
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
|
||||
return try parser.eventStopImmediatePropagation(self);
|
||||
}
|
||||
|
||||
pub fn _preventDefault(self: *parser.Event) !void {
|
||||
return try parser.eventPreventDefault(self);
|
||||
}
|
||||
};
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var common = [_]Case{
|
||||
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "let para = document.getElementById('para')", .ex = "undefined" },
|
||||
.{ .src = "var nb = 0; var evt", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &common);
|
||||
|
||||
var basic = [_]Case{
|
||||
.{ .src =
|
||||
\\content.addEventListener('target',
|
||||
\\function(e) {
|
||||
\\evt = e; nb = nb + 1;
|
||||
\\e.preventDefault();
|
||||
\\})
|
||||
, .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", .ex = "false" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
.{ .src = "evt.target === content", .ex = "true" },
|
||||
.{ .src = "evt.bubbles", .ex = "true" },
|
||||
.{ .src = "evt.cancelable", .ex = "true" },
|
||||
.{ .src = "evt.defaultPrevented", .ex = "true" },
|
||||
.{ .src = "evt.isTrusted", .ex = "true" },
|
||||
.{ .src = "evt.timestamp > 1704063600", .ex = "true" }, // 2024/01/01 00:00
|
||||
// event.type, event.currentTarget, event.phase checked in EventTarget
|
||||
};
|
||||
try checkCases(js_env, &basic);
|
||||
|
||||
var stop = [_]Case{
|
||||
.{ .src = "nb = 0", .ex = "0" },
|
||||
.{ .src =
|
||||
\\content.addEventListener('stop',
|
||||
\\function(e) {
|
||||
\\e.stopPropagation();
|
||||
\\nb = nb + 1;
|
||||
\\}, true)
|
||||
, .ex = "undefined" },
|
||||
// the following event listener will not be invoked
|
||||
.{ .src =
|
||||
\\para.addEventListener('stop',
|
||||
\\function(e) {
|
||||
\\nb = nb + 1;
|
||||
\\})
|
||||
, .ex = "undefined" },
|
||||
.{ .src = "para.dispatchEvent(new Event('stop'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at content event listener
|
||||
};
|
||||
try checkCases(js_env, &stop);
|
||||
|
||||
var stop_immediate = [_]Case{
|
||||
.{ .src = "nb = 0", .ex = "0" },
|
||||
.{ .src =
|
||||
\\content.addEventListener('immediate',
|
||||
\\function(e) {
|
||||
\\e.stopImmediatePropagation();
|
||||
\\nb = nb + 1;
|
||||
\\})
|
||||
, .ex = "undefined" },
|
||||
// the following event listener will not be invoked
|
||||
.{ .src =
|
||||
\\content.addEventListener('immediate',
|
||||
\\function(e) {
|
||||
\\nb = nb + 1;
|
||||
\\})
|
||||
, .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('immediate'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at first content event listener
|
||||
};
|
||||
try checkCases(js_env, &stop_immediate);
|
||||
|
||||
var legacy = [_]Case{
|
||||
.{ .src = "nb = 0", .ex = "0" },
|
||||
.{ .src =
|
||||
\\content.addEventListener('legacy',
|
||||
\\function(e) {
|
||||
\\evt = e; nb = nb + 1;
|
||||
\\})
|
||||
, .ex = "undefined" },
|
||||
.{ .src = "let evtLegacy = document.createEvent('Event')", .ex = "undefined" },
|
||||
.{ .src = "evtLegacy.initEvent('legacy')", .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(evtLegacy)", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
};
|
||||
try checkCases(js_env, &legacy);
|
||||
|
||||
var remove = [_]Case{
|
||||
.{ .src = "var nb = 0; var evt = null; function cbk(event) { nb ++; evt=event; }", .ex = "undefined" },
|
||||
.{ .src = "document.addEventListener('count', cbk)", .ex = "undefined" },
|
||||
.{ .src = "document.removeEventListener('count', cbk)", .ex = "undefined" },
|
||||
.{ .src = "document.dispatchEvent(new Event('count'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "0" },
|
||||
};
|
||||
try checkCases(js_env, &remove);
|
||||
}
|
||||
|
||||
pub const EventHandler = struct {
|
||||
fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
|
||||
// TODO get the allocator by another way?
|
||||
var res = CallbackResult.init(data.cbk.nat_ctx.alloc);
|
||||
defer res.deinit();
|
||||
|
||||
if (event) |evt| {
|
||||
data.cbk.trycall(.{
|
||||
Event.toInterface(evt) catch unreachable,
|
||||
}, &res) catch |e| log.err("event handler error: {any}", .{e});
|
||||
} else {
|
||||
data.cbk.trycall(.{event}, &res) catch |e| log.err("event handler error (null event): {any}", .{e});
|
||||
}
|
||||
|
||||
// in case of function error, we log the result and the trace.
|
||||
if (!res.success) {
|
||||
log.info("event handler error try catch: {s}", .{res.result orelse "unknown"});
|
||||
log.debug("{s}", .{res.stack orelse "no stack trace"});
|
||||
}
|
||||
}
|
||||
}.handle;
|
||||
@@ -102,7 +102,7 @@ pub fn main() !void {
|
||||
// page
|
||||
const page = try session.createPage(null);
|
||||
|
||||
_ = page.navigate(url, null) catch |err| switch (err) {
|
||||
_ = page.navigate(url, .{}) catch |err| switch (err) {
|
||||
error.UnsupportedUriScheme, error.UriMissingHost => {
|
||||
log.err("'{s}' is not a valid URL ({any})\n", .{ url, err });
|
||||
return args.printUsageAndExit(false);
|
||||
|
||||
@@ -1,10 +1,17 @@
|
||||
const URL = @import("url.zig").URL;
|
||||
const browser = @import("browser/browser.zig");
|
||||
|
||||
pub const Notification = union(enum) {
|
||||
page_navigate: PageEvent,
|
||||
page_navigated: PageEvent,
|
||||
page_navigate: PageNavigate,
|
||||
page_navigated: PageNavigated,
|
||||
|
||||
pub const PageEvent = struct {
|
||||
pub const PageNavigate = struct {
|
||||
timestamp: u32,
|
||||
url: *const URL,
|
||||
reason: browser.NavigateReason,
|
||||
};
|
||||
|
||||
pub const PageNavigated = struct {
|
||||
timestamp: u32,
|
||||
url: *const URL,
|
||||
};
|
||||
|
||||
@@ -48,7 +48,7 @@ pub const URL = struct {
|
||||
return parse(buf.items, null);
|
||||
}
|
||||
|
||||
// Above, in `parse, we error if a host doesn't exist
|
||||
// Above, in `parse`, we error if a host doesn't exist
|
||||
// In other words, we can't have a URL with a null host.
|
||||
pub fn host(self: *const URL) []const u8 {
|
||||
return self.uri.host.?.percent_encoded;
|
||||
|
||||
Reference in New Issue
Block a user