Skip to content

Commit 66ec087

Browse files
Merge pull request #516 from karlseguin/javascript_anchor_click
Add zig listener support to netsurf event handler
2 parents 16a30fa + 3134ff8 commit 66ec087

File tree

11 files changed

+556
-148
lines changed

11 files changed

+556
-148
lines changed

src/browser/browser.zig

Lines changed: 120 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,10 @@ pub const Session = struct {
134134
storage_shed: storage.Shed,
135135
cookie_jar: storage.CookieJar,
136136

137+
// arbitrary that we pass to the inspector, which the inspector will include
138+
// in any response/event that it emits.
139+
aux_data: ?[]const u8 = null,
140+
137141
page: ?Page = null,
138142
http_client: *http.Client,
139143

@@ -158,6 +162,7 @@ pub const Session = struct {
158162
const allocator = app.allocator;
159163
self.* = .{
160164
.app = app,
165+
.aux_data = null,
161166
.browser = browser,
162167
.notify_ctx = any_ctx,
163168
.inspector = undefined,
@@ -250,8 +255,12 @@ pub const Session = struct {
250255
// load polyfills
251256
try polyfill.load(self.arena.allocator(), self.executor);
252257

258+
if (aux_data) |ad| {
259+
self.aux_data = try self.arena.allocator().dupe(u8, ad);
260+
}
261+
253262
// inspector
254-
self.contextCreated(page, aux_data);
263+
self.contextCreated(page);
255264

256265
return page;
257266
}
@@ -279,9 +288,28 @@ pub const Session = struct {
279288
return &(self.page orelse return null);
280289
}
281290

282-
fn contextCreated(self: *Session, page: *Page, aux_data: ?[]const u8) void {
291+
fn pageNavigate(self: *Session, url_string: []const u8) !void {
292+
// currently, this is only called from the page, so let's hope
293+
// it isn't null!
294+
std.debug.assert(self.page != null);
295+
296+
// can't use the page arena, because we're about to reset it
297+
// and don't want to use the session's arena, because that'll start to
298+
// look like a leak if we navigate from page to page a lot.
299+
var buf: [1024]u8 = undefined;
300+
var fba = std.heap.FixedBufferAllocator.init(&buf);
301+
const url = try self.page.?.url.?.resolve(fba.allocator(), url_string);
302+
303+
self.removePage();
304+
var page = try self.createPage(null);
305+
return page.navigate(url, .{
306+
.reason = .anchor,
307+
});
308+
}
309+
310+
fn contextCreated(self: *Session, page: *Page) void {
283311
log.debug("inspector context created", .{});
284-
self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", aux_data);
312+
self.inspector.contextCreated(self.executor, "", (page.origin() catch "://") orelse "://", self.aux_data);
285313
}
286314

287315
fn notify(self: *const Session, notification: *const Notification) void {
@@ -361,7 +389,7 @@ pub const Page = struct {
361389
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
362390
// - aux_data: extra data forwarded to the Inspector
363391
// see Inspector.contextCreated
364-
pub fn navigate(self: *Page, request_url: URL, aux_data: ?[]const u8) !void {
392+
pub fn navigate(self: *Page, request_url: URL, opts: NavigateOpts) !void {
365393
const arena = self.arena;
366394
const session = self.session;
367395

@@ -387,7 +415,12 @@ pub const Page = struct {
387415
var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true });
388416
defer request.deinit();
389417

390-
session.notify(&.{ .page_navigate = .{ .url = url, .timestamp = timestamp() } });
418+
session.notify(&.{ .page_navigate = .{
419+
.url = url,
420+
.reason = opts.reason,
421+
.timestamp = timestamp(),
422+
} });
423+
391424
var response = try request.sendSync(.{});
392425

393426
// would be different than self.url in the case of a redirect
@@ -417,7 +450,7 @@ pub const Page = struct {
417450
var mime = try Mime.parse(arena, ct);
418451

419452
if (mime.isHTML()) {
420-
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8", aux_data);
453+
try self.loadHTMLDoc(&response, mime.charset orelse "utf-8");
421454
} else {
422455
log.info("non-HTML document: {s}", .{ct});
423456
var arr: std.ArrayListUnmanaged(u8) = .{};
@@ -428,44 +461,14 @@ pub const Page = struct {
428461
self.raw_data = arr.items;
429462
}
430463

431-
session.notify(&.{ .page_navigated = .{ .url = url, .timestamp = timestamp() } });
432-
}
433-
434-
pub const ClickResult = union(enum) {
435-
navigate: std.Uri,
436-
};
437-
438-
pub const MouseEvent = struct {
439-
x: i32,
440-
y: i32,
441-
type: Type,
442-
443-
const Type = enum {
444-
pressed,
445-
released,
446-
};
447-
};
448-
449-
pub fn mouseEvent(self: *Page, me: MouseEvent) !void {
450-
if (me.type != .pressed) {
451-
return;
452-
}
453-
454-
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return;
455-
456-
const event = try parser.mouseEventCreate();
457-
defer parser.mouseEventDestroy(event);
458-
try parser.mouseEventInit(event, "click", .{
459-
.bubbles = true,
460-
.cancelable = true,
461-
.x = me.x,
462-
.y = me.y,
463-
});
464-
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
464+
session.notify(&.{ .page_navigated = .{
465+
.url = url,
466+
.timestamp = timestamp(),
467+
} });
465468
}
466469

467470
// https://html.spec.whatwg.org/#read-html
468-
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8, aux_data: ?[]const u8) !void {
471+
fn loadHTMLDoc(self: *Page, reader: anytype, charset: []const u8) !void {
469472
const arena = self.arena;
470473

471474
// start netsurf memory arena.
@@ -481,6 +484,16 @@ pub const Page = struct {
481484
// save a document's pointer in the page.
482485
self.doc = doc;
483486

487+
const document_element = (try parser.documentGetDocumentElement(doc)) orelse return error.DocumentElementError;
488+
try parser.eventTargetAddZigListener(
489+
parser.toEventTarget(parser.Element, document_element),
490+
arena,
491+
"click",
492+
windowClicked,
493+
self,
494+
false,
495+
);
496+
484497
// TODO set document.readyState to interactive
485498
// https://html.spec.whatwg.org/#reporting-document-loading-status
486499

@@ -497,7 +510,7 @@ pub const Page = struct {
497510
// https://html.spec.whatwg.org/#read-html
498511

499512
// inspector
500-
session.contextCreated(self, aux_data);
513+
session.contextCreated(self);
501514

502515
{
503516
// update the sessions state
@@ -728,6 +741,61 @@ pub const Page = struct {
728741
return request;
729742
}
730743

744+
pub const MouseEvent = struct {
745+
x: i32,
746+
y: i32,
747+
type: Type,
748+
749+
const Type = enum {
750+
pressed,
751+
released,
752+
};
753+
};
754+
755+
pub fn mouseEvent(self: *Page, me: MouseEvent) !void {
756+
if (me.type != .pressed) {
757+
return;
758+
}
759+
760+
const element = self.renderer.getElementAtPosition(me.x, me.y) orelse return;
761+
762+
const event = try parser.mouseEventCreate();
763+
defer parser.mouseEventDestroy(event);
764+
try parser.mouseEventInit(event, "click", .{
765+
.bubbles = true,
766+
.cancelable = true,
767+
.x = me.x,
768+
.y = me.y,
769+
});
770+
_ = try parser.elementDispatchEvent(element, @ptrCast(event));
771+
}
772+
773+
fn windowClicked(ctx: *anyopaque, event: *parser.Event) void {
774+
const self: *Page = @alignCast(@ptrCast(ctx));
775+
self._windowClicked(event) catch |err| {
776+
log.err("window click handler: {}", .{err});
777+
};
778+
}
779+
780+
fn _windowClicked(self: *Page, event: *parser.Event) !void {
781+
const target = (try parser.eventTarget(event)) orelse return;
782+
783+
const node = parser.eventTargetToNode(target);
784+
if (try parser.nodeType(node) != .element) {
785+
return;
786+
}
787+
788+
const html_element: *parser.ElementHTML = @ptrCast(node);
789+
switch (try parser.elementHTMLGetTagType(html_element)) {
790+
.a => {
791+
const element: *parser.Element = @ptrCast(node);
792+
const href = (try parser.elementGetAttribute(element, "href")) orelse return;
793+
return self.session.pageNavigate(href);
794+
},
795+
else => {},
796+
}
797+
}
798+
731799
const Script = struct {
732800
element: *parser.Element,
733801
kind: Kind,
@@ -792,8 +860,17 @@ pub const Page = struct {
792860
};
793861
};
794862

863+
pub const NavigateReason = enum {
864+
anchor,
865+
address_bar,
866+
};
867+
868+
const NavigateOpts = struct {
869+
reason: NavigateReason = .address_bar,
870+
};
871+
795872
// provide very poor abstration to the rest of the code. In theory, we can change
796-
// the FlatRendere to a different implementation, and it'll all just work.
873+
// the FlatRenderer to a different implementation, and it'll all just work.
797874
pub const Renderer = FlatRenderer;
798875

799876
// This "renderer" positions elements in a single row in an unspecified order.

src/browser/dom/mutation_observer.zig

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -104,15 +104,15 @@ pub const MutationObserver = struct {
104104
arena,
105105
"DOMNodeInserted",
106106
EventHandler,
107-
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
107+
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
108108
false,
109109
);
110110
try parser.eventTargetAddEventListener(
111111
parser.toEventTarget(parser.Node, node),
112112
arena,
113113
"DOMNodeRemoved",
114114
EventHandler,
115-
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
115+
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
116116
false,
117117
);
118118
}
@@ -122,7 +122,7 @@ pub const MutationObserver = struct {
122122
arena,
123123
"DOMAttrModified",
124124
EventHandler,
125-
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
125+
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
126126
false,
127127
);
128128
}
@@ -132,7 +132,7 @@ pub const MutationObserver = struct {
132132
arena,
133133
"DOMCharacterDataModified",
134134
EventHandler,
135-
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
135+
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
136136
false,
137137
);
138138
}
@@ -142,7 +142,7 @@ pub const MutationObserver = struct {
142142
arena,
143143
"DOMSubtreeModified",
144144
EventHandler,
145-
.{ .cbk = self.cbk, .data = o, .deinitFunc = deinitFunc },
145+
.{ .cbk = self.cbk, .ctx = o, .deinitFunc = deinitFunc },
146146
false,
147147
);
148148
}
@@ -261,7 +261,7 @@ const EventHandler = struct {
261261
return false;
262262
}
263263

264-
fn handle(evt: ?*parser.Event, data: parser.EventHandlerData) void {
264+
fn handle(evt: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
265265
if (evt == null) return;
266266

267267
var mrs: MutationRecords = .{};
@@ -277,7 +277,7 @@ const EventHandler = struct {
277277
const node = parser.eventTargetToNode(et);
278278

279279
// retrieve the observer from the data.
280-
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.data));
280+
const o: *MutationObserver.Observer = @ptrCast(@alignCast(data.ctx));
281281

282282
if (!apply(o, node)) return;
283283

src/browser/events/event.zig

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ pub const Event = struct {
136136
};
137137

138138
pub const EventHandler = struct {
139-
fn handle(event: ?*parser.Event, data: parser.EventHandlerData) void {
139+
fn handle(event: ?*parser.Event, data: *const parser.JSEventHandlerData) void {
140140
var result: Callback.Result = undefined;
141141
data.cbk.tryCall(.{if (event) |evt| Event.toInterface(evt) catch unreachable else null}, &result) catch {
142142
log.err("event handler error: {s}", .{result.exception});

src/browser/html/elements.zig

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,18 @@ pub const HTMLElement = struct {
131131
// attach the text node.
132132
_ = try parser.nodeAppendChild(n, @as(*parser.Node, @ptrCast(t)));
133133
}
134+
135+
pub fn _click(e: *parser.ElementHTML) !void {
136+
const event = try parser.mouseEventCreate();
137+
defer parser.mouseEventDestroy(event);
138+
try parser.mouseEventInit(event, "click", .{
139+
.x = 0,
140+
.y = 0,
141+
.bubbles = true,
142+
.cancelable = true,
143+
});
144+
_ = try parser.elementDispatchEvent(@ptrCast(e), @ptrCast(event));
145+
}
134146
};
135147

136148
// Deprecated HTMLElements in Chrome (2023/03/15)
@@ -1050,4 +1062,12 @@ test "Browser.HTML.Element" {
10501062
.{ "document.getElementById('content').innerText", "foo" },
10511063
.{ "document.getElementById('content').innerHTML = backup; true;", "true" },
10521064
}, .{});
1065+
1066+
try runner.testCases(&.{
1067+
.{ "let click_count = 0;", "undefined" },
1068+
.{ "let clickCbk = function() { click_count++ }", "undefined" },
1069+
.{ "document.getElementById('content').addEventListener('click', clickCbk);", "undefined" },
1070+
.{ "document.getElementById('content').click()", "undefined" },
1071+
.{ "click_count", "1" },
1072+
}, .{});
10531073
}

0 commit comments

Comments
 (0)