From 086faf44fc7f9506d972e3b071df88e7371135b6 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 11 Feb 2026 19:35:10 +0800 Subject: [PATCH] Trigger inline handlers This is a follow up / fix to https://github.com/lightpanda-io/browser/pull/1487 In that PR we triggered a "load" event for special elements, and as part of that we triggered both the "onload" attribute via dispatchWithFunction and normal bubbling with dispatch. This PR applies this change generically and holistically. For example, if an "abort" event is raised, the "onabort" attribute will be generated for that element. Importantly, this gets executed in the correct dispatch order and respect event cancellation (stopPropagation and stopImmediatePropagation). --- src/browser/EventManager.zig | 51 +++++++- src/browser/Page.zig | 12 -- src/browser/tests/events.html | 127 +++++++++++++++++++ src/browser/webapi/element/Html.zig | 7 +- src/browser/webapi/global_event_handlers.zig | 21 +++ 5 files changed, 196 insertions(+), 22 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 5f169514..441f32e3 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -329,13 +329,36 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: // Phase 2: At target event._event_phase = .at_target; const target_et = target.asEventTarget(); - if (self.lookup.get(.{ - .type_string = event._type_string, - .event_target = @intFromPtr(target_et), - })) |list| { - try self.dispatchPhase(list, target_et, event, was_handled, null); - if (event._stop_propagation) { - return; + + blk: { + // Get inline handler (e.g., onclick property) for this target + if (self.getInlineHandler(target_et, event)) |inline_handler| { + was_handled.* = true; + event._current_target = target_et; + + var ls: js.Local.Scope = undefined; + self.page.js.localScope(&ls); + defer ls.deinit(); + + try ls.toLocal(inline_handler).callWithThis(void, target_et, .{event}); + + if (event._stop_propagation) { + return; + } + + if (event._stop_immediate_propagation) { + break :blk; + } + } + + if (self.lookup.get(.{ + .type_string = event._type_string, + .event_target = @intFromPtr(target_et), + })) |list| { + try self.dispatchPhase(list, target_et, event, was_handled, null); + if (event._stop_propagation) { + return; + } } } @@ -460,6 +483,20 @@ fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: return self.dispatchPhase(list, current_target, event, was_handled, null); } +fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global { + const global_event_handlers = @import("webapi/global_event_handlers.zig"); + const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null; + + // Look up the inline handler for this target + const Element = @import("webapi/Element.zig"); + const element = switch (target._type) { + .node => |n| n.is(Element) orelse return null, + else => return null, + }; + + return self.page.getAttrListener(element, handler_type); +} + fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void { // If we're in a dispatch, defer removal to avoid invalidating iteration if (self.dispatch_depth > 0) { diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 175cd3a9..e10b76e1 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -710,18 +710,6 @@ fn _documentIsComplete(self: *Page) !void { for (self._to_load.items) |element| { const event = try Event.initTrusted(comptime .wrap("load"), .{}, self); defer if (!event._v8_handoff) event.deinit(false); - - // Dispatch inline event. - blk: { - const html_element = element.is(HtmlElement) orelse break :blk; - - const listener = (try html_element.getOnLoad(self)) orelse break :blk; - ls.toLocal(listener).call(void, .{}) catch |err| { - log.warn(.event, "inline load event", .{ .element = element, .err = err }); - }; - } - - // Dispatch events registered to event manager. try self._event_manager.dispatch(element.asEventTarget(), event); } diff --git a/src/browser/tests/events.html b/src/browser/tests/events.html index 08319569..e463300e 100644 --- a/src/browser/tests/events.html +++ b/src/browser/tests/events.html @@ -635,3 +635,130 @@ // https://github.com/lightpanda-io/browser/pull/1316 testing.expectError('TypeError', () => MessageEvent('')); + +
+ + +
+ + +
+ + +
+ + +
+ diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index bbb8a050..ab393abe 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -329,14 +329,15 @@ pub fn click(self: *HtmlElement, page: *Page) !void { else => {}, } - const event = try @import("../event/MouseEvent.zig").init("click", .{ + const event = (try @import("../event/MouseEvent.zig").init("click", .{ .bubbles = true, .cancelable = true, .composed = true, .clientX = 0, .clientY = 0, - }, page); - try page._event_manager.dispatch(self.asEventTarget(), event.asEvent()); + }, page)).asEvent(); + defer if (!event._v8_handoff) event.deinit(false); + try page._event_manager.dispatch(self.asEventTarget(), event); } fn getAttributeFunction( diff --git a/src/browser/webapi/global_event_handlers.zig b/src/browser/webapi/global_event_handlers.zig index 44123a50..64843c26 100644 --- a/src/browser/webapi/global_event_handlers.zig +++ b/src/browser/webapi/global_event_handlers.zig @@ -164,3 +164,24 @@ pub const Handler = enum(u7) { onwaiting, onwheel, }; + +const typeToHandler = std.StaticStringMap(Handler).initComptime(blk: { + const fields = std.meta.fields(Handler); + var entries: [fields.len]struct { []const u8, Handler } = undefined; + for (fields, 0..) |field, i| { + entries[i] = .{ field.name[2..], @enumFromInt(field.value) }; + } + break :blk entries; +}); + +pub fn fromEventType(typ: []const u8) ?Handler { + return typeToHandler.get(typ); +} + +const testing = @import("../../testing.zig"); +test "GlobalEventHandlers: fromEventType" { + try testing.expectEqual(.onabort, fromEventType("abort")); + try testing.expectEqual(.onselect, fromEventType("select")); + try testing.expectEqual(null, fromEventType("")); + try testing.expectEqual(null, fromEventType("unknown")); +}