Merge pull request #1389 from lightpanda-io/nikneym/image-src-dispatch

`Image` & `Style`: Dispatch `load` event
This commit is contained in:
Halil Durak
2026-02-05 15:23:38 +03:00
committed by GitHub
4 changed files with 115 additions and 3 deletions

View File

@@ -41,8 +41,10 @@ const Parser = @import("parser/Parser.zig");
const URL = @import("URL.zig");
const Node = @import("webapi/Node.zig");
const Event = @import("webapi/Event.zig");
const EventTarget = @import("webapi/EventTarget.zig");
const CData = @import("webapi/CData.zig");
const Element = @import("webapi/Element.zig");
const HtmlElement = @import("webapi/element/Html.zig");
const Window = @import("webapi/Window.zig");
const Location = @import("webapi/Location.zig");
const Document = @import("webapi/Document.zig");
@@ -124,6 +126,10 @@ _element_assigned_slots: Element.AssignedSlotLookup = .{},
/// ```
_element_attr_listeners: GlobalEventHandlersLookup = .{},
/// `load` events that'll be fired before window's `load` event.
/// A call to `documentIsComplete` (which calls `_documentIsComplete`) resets it.
_to_load: std.ArrayList(*Element) = .{},
_script_manager: ScriptManager,
// List of active MutationObservers
@@ -335,6 +341,8 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._element_attr_listeners = .{};
self._to_load = .{};
self._notified_network_idle = .init;
self._notified_network_almost_idle = .init;
@@ -690,15 +698,34 @@ pub fn documentIsComplete(self: *Page) void {
fn _documentIsComplete(self: *Page) !void {
self.document._ready_state = .complete;
// dispatch window.load event
const event = try Event.initTrusted("load", .{}, self);
// this event is weird, it's dispatched directly on the window, but
// with the document as the target
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
// Dispatch `_to_load` events before window.load.
for (self._to_load.items) |element| {
const maybe_inline_listener = self.getAttrListener(element, .onload);
try self._event_manager.dispatchWithFunction(
element.asEventTarget(),
event,
ls.toLocal(maybe_inline_listener),
.{ .context = "Page dispatch load events" },
);
if (comptime IS_DEBUG) {
log.debug(.page, "load event for element", .{ .element = element });
}
}
// `_to_load` can be cleaned here.
self._to_load.clearAndFree(self.arena);
// Dispatch window.load event.
// This event is weird, it's dispatched directly on the window, but
// with the document as the target.
event._target = self.document.asEventTarget();
try self._event_manager.dispatchWithFunction(
self.window.asEventTarget(),

View File

@@ -97,3 +97,62 @@
testing.expectEqual('lazy', img.getAttribute('loading'));
}
</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,10 @@ const URL = @import("../../../URL.zig");
const Node = @import("../../Node.zig");
const Element = @import("../../Element.zig");
const HtmlElement = @import("../Html.zig");
const Event = @import("../../Event.zig");
const log = @import("../../../../log.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const Image = @This();
_proto: *HtmlElement,
@@ -115,6 +119,19 @@ pub const JsApi = struct {
pub const loading = bridge.accessor(Image.getLoading, Image.setLoading, .{});
};
pub const Build = struct {
pub fn created(node: *Node, page: *Page) !void {
const self = node.as(Image);
const image = self.asElement();
// Exit if src not set.
// TODO: We might want to check if src point to valid image.
_ = image.getAttributeSafe(comptime .wrap("src")) orelse return;
// Push to `_to_load` to dispatch load event just before window load event.
return page._to_load.append(page.arena, image);
}
};
const testing = @import("../../../../testing.zig");
test "WebApi: HTML.Image" {
try testing.htmlRunner("element/html/image.html", .{});

View File

@@ -96,6 +96,15 @@ pub const JsApi = struct {
pub const sheet = bridge.accessor(Style.getSheet, null, .{});
};
pub const Build = struct {
pub fn created(node: *Node, page: *Page) !void {
const self = node.as(Style);
const style = self.asElement();
// Push to `_to_load` to dispatch load event just before window load event.
return page._to_load.append(page.arena, style);
}
};
const testing = @import("../../../../testing.zig");
test "WebApi: Style" {
try testing.htmlRunner("element/html/style.html", .{});