mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-03-22 12:44:43 +00:00
Merge pull request #1524 from lightpanda-io/trigger_inline_handlers
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
Some checks failed
e2e-test / zig build release (push) Has been cancelled
e2e-test / demo-scripts (push) Has been cancelled
e2e-test / cdp-and-hyperfine-bench (push) Has been cancelled
e2e-test / perf-fmt (push) Has been cancelled
e2e-test / browser fetch (push) Has been cancelled
zig-test / zig test using v8 in debug mode (push) Has been cancelled
zig-test / zig test (push) Has been cancelled
zig-test / perf-fmt (push) Has been cancelled
wpt / web platform tests json output (push) Has been cancelled
wpt / perf-fmt (push) Has been cancelled
e2e-integration-test / zig build release (push) Has been cancelled
e2e-integration-test / demo-integration-scripts (push) Has been cancelled
Trigger inline handlers
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