EventManager: add hasListener

Not sure if this should be in `EventTarget` or `EventManager`, here goes nothing.

`Image`: dispatch `load` event when `src` set
add load event test

remove `hasListener`

let `Scheduler` dispatch `load` event

Simulates async nature.

update test

free `args` when done

implement `load` event dispatch for `<img>` tags

This dispatches `load` events tied to `EventManager` but not the `onload` for some reason...

`"load"` event must be dispatched even if `onload` not set

Resolves the bug that cause event listeners added through `EventTarget` not executing if `onload` not set.

add `onload` getter/setter for `Image`

prefer `attributeChange` to run side-effects

This should give more consistent results than using `setSrc`.
add inline `<img src="..." />` test

`Image`: prefer `inline_lookup` for `onload`

remove incorrect URL check + prefer 0ms in `Scheduler`

change after rebase
This commit is contained in:
Halil Durak
2026-01-20 19:11:36 +03:00
parent 9d6f9aae9a
commit 988f499723
2 changed files with 130 additions and 0 deletions

View File

@@ -97,3 +97,62 @@
testing.expectEqual('lazy', img.getAttribute('loading')); testing.expectEqual('lazy', img.getAttribute('loading'));
} }
</script> </script>
<script id="load-trigger-event">
{
const img = document.createElement("img");
let count = 0;
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
testing.expectEqual(true, count < 3);
count++;
testing.expectEqual(false, bubbles);
testing.expectEqual(false, cancelBubble);
testing.expectEqual(false, cancelable);
testing.expectEqual(false, composed);
testing.expectEqual(true, isTrusted);
testing.expectEqual(img, target);
});
for (let i = 0; i < 3; i++) {
img.src = "https://cdn.lightpanda.io/website/assets/images/docs/hn.png";
testing.expectEqual("https://cdn.lightpanda.io/website/assets/images/docs/hn.png", img.src);
}
// Make sure count is incremented asynchronously.
testing.expectEqual(0, count);
}
</script>
<img
id="inline-img"
src="https://cdn.lightpanda.io/website/assets/images/docs/hn.png"
onload="(() => testing.expectEqual(true, true))()"
/>
<script id="inline-on-load">
{
const img = document.getElementById("inline-img");
testing.expectEqual(true, img.onload instanceof Function);
// Also call inline to double check.
img.onload();
// Make sure ones attached with `addEventListener` also executed.
testing.async(async () => {
const result = await new Promise(resolve => {
img.addEventListener("load", ({ bubbles, cancelBubble, cancelable, composed, isTrusted, target }) => {
testing.expectEqual(false, bubbles);
testing.expectEqual(false, cancelBubble);
testing.expectEqual(false, cancelable);
testing.expectEqual(false, composed);
testing.expectEqual(true, isTrusted);
testing.expectEqual(img, target);
return resolve(true);
});
});
testing.expectEqual(true, result);
});
}
</script>

View File

@@ -5,6 +5,8 @@ const URL = @import("../../../URL.zig");
const Node = @import("../../Node.zig"); const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig"); const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig"); const HtmlElement = @import("../Html.zig");
const Event = @import("../../Event.zig");
const log = @import("../../../../log.zig");
const Image = @This(); const Image = @This();
_proto: *HtmlElement, _proto: *HtmlElement,
@@ -47,6 +49,41 @@ pub fn getSrc(self: *const Image, page: *Page) ![]const u8 {
pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void { pub fn setSrc(self: *Image, value: []const u8, page: *Page) !void {
try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(value), page); try self.asElement().setAttributeSafe(comptime .wrap("src"), .wrap(value), page);
const event_target = self.asNode().asEventTarget();
// Have to do this since `Scheduler` only allow passing a single arg.
const SetSrcCallback = struct {
page: *Page,
event_target: *@import("../../EventTarget.zig"),
};
const args = try page._factory.create(SetSrcCallback{
.page = page,
.event_target = event_target,
});
errdefer page._factory.destroy(args);
// We don't actually fetch the media, here we fake the load call.
try page.scheduler.add(
args,
struct {
fn wrap(raw: *anyopaque) anyerror!?u32 {
const _args: *SetSrcCallback = @ptrCast(@alignCast(raw));
const _page = _args.page;
defer _page._factory.destroy(_args);
// Dispatch.
const event = try Event.initTrusted("load", .{}, _page);
try _page._event_manager.dispatch(_args.event_target, event);
return null;
}
}.wrap,
25,
.{
.low_priority = false,
.name = "Image.setSrc",
},
);
} }
pub fn getAlt(self: *const Image) []const u8 { pub fn getAlt(self: *const Image) []const u8 {
@@ -115,6 +152,40 @@ pub const JsApi = struct {
pub const loading = bridge.accessor(Image.getLoading, Image.setLoading, .{}); pub const loading = bridge.accessor(Image.getLoading, Image.setLoading, .{});
}; };
/// Argument passed to `dispatchLoadEvent`.
const CallbackParams = struct { page: *Page, element: *Element };
/// Callback passed to `Scheduler` to execute load listeners.
fn dispatchLoadEvent(raw: *anyopaque) !?u32 {
const _args: *CallbackParams = @ptrCast(@alignCast(raw));
const _page = _args.page;
defer _page._factory.destroy(_args);
const _element = _args.element;
const _img = _element.as(Image);
const event_target = _element.asEventTarget();
const event = try Event.initTrusted("load", .{}, _page);
// If onload provided, dispatch with it.
if (_img.getOnLoad(_page)) |_on_load| {
var ls: js.Local.Scope = undefined;
_page.js.localScope(&ls);
defer ls.deinit();
try _page._event_manager.dispatchWithFunction(
event_target,
event,
_on_load.local(&ls.local),
.{ .context = "Image.onload" },
);
return null;
}
// Dispatch to addEventListener listeners.
try _page._event_manager.dispatch(event_target, event);
return null;
}
const testing = @import("../../../../testing.zig"); const testing = @import("../../../../testing.zig");
test "WebApi: HTML.Image" { test "WebApi: HTML.Image" {
try testing.htmlRunner("element/html/image.html", .{}); try testing.htmlRunner("element/html/image.html", .{});