mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 12:44:43 +00:00
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).
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -635,3 +635,130 @@
|
||||
// https://github.com/lightpanda-io/browser/pull/1316
|
||||
testing.expectError('TypeError', () => MessageEvent(''));
|
||||
</script>
|
||||
|
||||
<div id=inline_parent><div id=inline_child></div></div>
|
||||
<script id=inlineHandlerReceivesEvent>
|
||||
// Test that inline onclick handler receives the event object
|
||||
{
|
||||
const inline_child = $('#inline_child');
|
||||
let receivedType = null;
|
||||
let receivedTarget = null;
|
||||
let receivedCurrentTarget = null;
|
||||
|
||||
inline_child.onclick = function(e) {
|
||||
// Capture values DURING handler execution
|
||||
receivedType = e.type;
|
||||
receivedTarget = e.target;
|
||||
receivedCurrentTarget = e.currentTarget;
|
||||
};
|
||||
|
||||
inline_child.click();
|
||||
|
||||
testing.expectEqual('click', receivedType);
|
||||
testing.expectEqual(inline_child, receivedTarget);
|
||||
testing.expectEqual(inline_child, receivedCurrentTarget);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=inline_order_parent><div id=inline_order_child></div></div>
|
||||
<script id=inlineHandlerOrder>
|
||||
// Test that inline handler executes in proper order with addEventListener
|
||||
{
|
||||
const inline_order_child = $('#inline_order_child');
|
||||
const inline_order_parent = $('#inline_order_parent');
|
||||
const order = [];
|
||||
|
||||
// Capture listener on parent
|
||||
inline_order_parent.addEventListener('click', () => order.push('parent-capture'), true);
|
||||
|
||||
// Inline handler on child (should execute at target phase)
|
||||
inline_order_child.onclick = () => order.push('child-onclick');
|
||||
|
||||
// addEventListener on child (should execute at target phase, after onclick)
|
||||
inline_order_child.addEventListener('click', () => order.push('child-listener'));
|
||||
|
||||
// Bubble listener on parent
|
||||
inline_order_parent.addEventListener('click', () => order.push('parent-bubble'));
|
||||
|
||||
inline_order_child.click();
|
||||
|
||||
// Expected order: capture, then onclick, then addEventListener, then bubble
|
||||
testing.expectEqual('parent-capture', order[0]);
|
||||
testing.expectEqual('child-onclick', order[1]);
|
||||
testing.expectEqual('child-listener', order[2]);
|
||||
testing.expectEqual('parent-bubble', order[3]);
|
||||
testing.expectEqual(4, order.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=inline_prevent><div id=inline_prevent_child></div></div>
|
||||
<script id=inlineHandlerPreventDefault>
|
||||
// Test that inline handler can preventDefault and it affects addEventListener listeners
|
||||
{
|
||||
const inline_prevent_child = $('#inline_prevent_child');
|
||||
let preventDefaultCalled = false;
|
||||
let listenerSawPrevented = false;
|
||||
|
||||
inline_prevent_child.onclick = function(e) {
|
||||
e.preventDefault();
|
||||
preventDefaultCalled = true;
|
||||
};
|
||||
|
||||
inline_prevent_child.addEventListener('click', (e) => {
|
||||
listenerSawPrevented = e.defaultPrevented;
|
||||
});
|
||||
|
||||
const result = inline_prevent_child.dispatchEvent(new MouseEvent('click', {
|
||||
bubbles: true,
|
||||
cancelable: true
|
||||
}));
|
||||
|
||||
testing.expectEqual(true, preventDefaultCalled);
|
||||
testing.expectEqual(true, listenerSawPrevented);
|
||||
testing.expectEqual(false, result); // dispatchEvent returns false when prevented
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=inline_stop_parent><div id=inline_stop_child></div></div>
|
||||
<script id=inlineHandlerStopPropagation>
|
||||
// Test that inline handler can stopPropagation
|
||||
{
|
||||
const inline_stop_child = $('#inline_stop_child');
|
||||
const inline_stop_parent = $('#inline_stop_parent');
|
||||
let childCalled = false;
|
||||
let parentCalled = false;
|
||||
|
||||
inline_stop_child.onclick = function(e) {
|
||||
childCalled = true;
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
inline_stop_parent.addEventListener('click', () => {
|
||||
parentCalled = true;
|
||||
});
|
||||
|
||||
inline_stop_child.click();
|
||||
|
||||
testing.expectEqual(true, childCalled);
|
||||
testing.expectEqual(false, parentCalled); // Should not bubble to parent
|
||||
}
|
||||
</script>
|
||||
|
||||
<div id=inline_replace_test></div>
|
||||
<script id=inlineHandlerReplacement>
|
||||
// Test that setting onclick property replaces previous handler
|
||||
{
|
||||
const inline_replace_test = $('#inline_replace_test');
|
||||
let calls = [];
|
||||
|
||||
inline_replace_test.onclick = () => calls.push('first');
|
||||
inline_replace_test.click();
|
||||
|
||||
inline_replace_test.onclick = () => calls.push('second');
|
||||
inline_replace_test.click();
|
||||
|
||||
testing.expectEqual('first', calls[0]);
|
||||
testing.expectEqual('second', calls[1]);
|
||||
testing.expectEqual(2, calls.length);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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"));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user