mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-15 15:58:57 +00:00
Event.composedPath and adjusted target when crossing shadowroot boundary
This commit is contained in:
@@ -162,18 +162,36 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
|
||||
}
|
||||
|
||||
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
|
||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||
|
||||
var path_len: usize = 0;
|
||||
var path_buffer: [128]*EventTarget = undefined;
|
||||
|
||||
var node: ?*Node = target;
|
||||
while (node) |n| : (node = n._parent) {
|
||||
while (node) |n| {
|
||||
if (path_len >= path_buffer.len) break;
|
||||
path_buffer[path_len] = n.asEventTarget();
|
||||
path_len += 1;
|
||||
|
||||
// Check if this node is a shadow root
|
||||
if (n.is(ShadowRoot)) |shadow| {
|
||||
event._needs_retargeting = true;
|
||||
|
||||
// If event is not composed, stop at shadow boundary
|
||||
if (!event._composed) {
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise, jump to the shadow host and continue
|
||||
node = shadow._host.asNode();
|
||||
continue;
|
||||
}
|
||||
|
||||
node = n._parent;
|
||||
}
|
||||
|
||||
// Even though the window isn't part of the DOM, events always propagate
|
||||
// through it in the capture phase
|
||||
// through it in the capture phase (unless we stopped at a shadow boundary)
|
||||
if (path_len < path_buffer.len) {
|
||||
path_buffer[path_len] = self.page.window.asEventTarget();
|
||||
path_len += 1;
|
||||
@@ -257,6 +275,12 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
was_handled.* = true;
|
||||
event._current_target = current_target;
|
||||
|
||||
// Compute adjusted target for shadow DOM retargeting (only if needed)
|
||||
const original_target = event._target;
|
||||
if (event._needs_retargeting) {
|
||||
event._target = getAdjustedTarget(original_target, current_target);
|
||||
}
|
||||
|
||||
switch (listener.function) {
|
||||
.value => |value| try value.call(void, .{event}),
|
||||
.string => |string| {
|
||||
@@ -265,6 +289,11 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
},
|
||||
}
|
||||
|
||||
// Restore original target (only if we changed it)
|
||||
if (event._needs_retargeting) {
|
||||
event._target = original_target;
|
||||
}
|
||||
|
||||
if (listener.once) {
|
||||
self.removeListener(list, listener);
|
||||
}
|
||||
@@ -325,3 +354,56 @@ const Function = union(enum) {
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Computes the adjusted target for shadow DOM event retargeting
|
||||
// Returns the lowest shadow-including ancestor of original_target that is
|
||||
// also an ancestor-or-self of current_target
|
||||
fn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarget) ?*EventTarget {
|
||||
const ShadowRoot = @import("webapi/ShadowRoot.zig");
|
||||
|
||||
const orig_node = switch ((original_target orelse return null)._type) {
|
||||
.node => |n| n,
|
||||
else => return original_target,
|
||||
};
|
||||
const curr_node = switch (current_target._type) {
|
||||
.node => |n| n,
|
||||
else => return original_target,
|
||||
};
|
||||
|
||||
// Walk up from original target, checking if we can reach current target
|
||||
var node: ?*Node = orig_node;
|
||||
while (node) |n| {
|
||||
// Check if current_target is an ancestor of n (or n itself)
|
||||
if (isAncestorOrSelf(curr_node, n)) {
|
||||
return n.asEventTarget();
|
||||
}
|
||||
|
||||
// Cross shadow boundary if needed
|
||||
if (n.is(ShadowRoot)) |shadow| {
|
||||
node = shadow._host.asNode();
|
||||
continue;
|
||||
}
|
||||
|
||||
node = n._parent;
|
||||
}
|
||||
|
||||
return original_target;
|
||||
}
|
||||
|
||||
// Check if ancestor is an ancestor of (or the same as) node
|
||||
// WITHOUT crossing shadow boundaries (just regular DOM tree)
|
||||
fn isAncestorOrSelf(ancestor: *Node, node: *Node) bool {
|
||||
if (ancestor == node) {
|
||||
return true;
|
||||
}
|
||||
|
||||
var current: ?*Node = node._parent;
|
||||
while (current) |n| {
|
||||
if (n == ancestor) {
|
||||
return true;
|
||||
}
|
||||
current = n._parent;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ pub fn callWithThis(self: *const Function, comptime T: type, this: anytype, args
|
||||
|
||||
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
|
||||
if (result == null) {
|
||||
// std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
|
||||
std.debug.print("CB ERR: {s}\n", .{self.src() catch "???"});
|
||||
return error.JSExecCallback;
|
||||
}
|
||||
|
||||
|
||||
@@ -52,34 +52,135 @@
|
||||
</script>
|
||||
|
||||
<script id="event_composed_escapes_shadow">
|
||||
// @ZIGDOM TODO
|
||||
testing.expectEqual(true, true);
|
||||
{
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
shadow.innerHTML = '<button id="btn">Click</button>';
|
||||
document.body.appendChild(host);
|
||||
|
||||
// {
|
||||
// const host = document.createElement('div');
|
||||
// const shadow = host.attachShadow({ mode: 'open' });
|
||||
// shadow.innerHTML = '<button id="btn">Click</button>';
|
||||
// document.body.appendChild(host);
|
||||
const button = shadow.getElementById('btn');
|
||||
|
||||
// const button = shadow.getElementById('btn');
|
||||
let hostCalled = false;
|
||||
let hostTarget = null;
|
||||
|
||||
// let hostCalled = false;
|
||||
// let hostTarget = null;
|
||||
host.addEventListener('click', (e) => {
|
||||
hostCalled = true;
|
||||
hostTarget = e.target;
|
||||
});
|
||||
|
||||
// host.addEventListener('click', (e) => {
|
||||
// hostCalled = true;
|
||||
// hostTarget = e.target;
|
||||
// });
|
||||
// With composed:true, event SHOULD escape shadow tree
|
||||
const event = new Event('click', { bubbles: true, composed: true });
|
||||
button.dispatchEvent(event);
|
||||
|
||||
// // With composed:true, event SHOULD escape shadow tree
|
||||
// const event = new Event('click', { bubbles: true, composed: true });
|
||||
// button.dispatchEvent(event);
|
||||
testing.expectEqual(true, hostCalled);
|
||||
|
||||
// testing.expectEqual(true, hostCalled);
|
||||
// Event target should be retargeted to host when outside shadow tree
|
||||
testing.expectEqual(host, hostTarget);
|
||||
|
||||
// // Event target should be retargeted to host when outside shadow tree
|
||||
// testing.expectEqual(host, hostTarget);
|
||||
|
||||
// host.remove();
|
||||
// }
|
||||
host.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="composedPath_basic">
|
||||
{
|
||||
const div1 = document.createElement('div');
|
||||
const div2 = document.createElement('div');
|
||||
const button = document.createElement('button');
|
||||
|
||||
div1.appendChild(div2);
|
||||
div2.appendChild(button);
|
||||
document.body.appendChild(div1);
|
||||
|
||||
let capturedPath = null;
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
capturedPath = e.composedPath();
|
||||
});
|
||||
|
||||
const event = new Event('click', { bubbles: true });
|
||||
button.dispatchEvent(event);
|
||||
|
||||
testing.expectEqual(7, capturedPath.length);
|
||||
testing.expectEqual(button, capturedPath[0]);
|
||||
testing.expectEqual(div2, capturedPath[1]);
|
||||
testing.expectEqual(div1, capturedPath[2]);
|
||||
testing.expectEqual(document.body, capturedPath[3]);
|
||||
testing.expectEqual(document.documentElement, capturedPath[4]);
|
||||
testing.expectEqual(document, capturedPath[5]);
|
||||
testing.expectEqual(window, capturedPath[6]);
|
||||
|
||||
div1.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="composedPath_after_dispatch">
|
||||
{
|
||||
const button = document.createElement('button');
|
||||
document.body.appendChild(button);
|
||||
|
||||
const event = new Event('click', { bubbles: true });
|
||||
button.dispatchEvent(event);
|
||||
|
||||
const path = event.composedPath();
|
||||
testing.expectEqual(0, path.length);
|
||||
|
||||
button.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="composedPath_with_shadow_not_composed">
|
||||
{
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
const button = document.createElement('button');
|
||||
|
||||
shadow.appendChild(button);
|
||||
document.body.appendChild(host);
|
||||
|
||||
let capturedPath = null;
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
capturedPath = e.composedPath();
|
||||
});
|
||||
|
||||
const event = new Event('click', { bubbles: true });
|
||||
button.dispatchEvent(event);
|
||||
|
||||
testing.expectEqual(2, capturedPath.length);
|
||||
testing.expectEqual(button, capturedPath[0]);
|
||||
testing.expectEqual(shadow, capturedPath[1]);
|
||||
|
||||
host.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script id="composedPath_with_shadow_composed">
|
||||
{
|
||||
const host = document.createElement('div');
|
||||
const shadow = host.attachShadow({ mode: 'open' });
|
||||
const button = document.createElement('button');
|
||||
|
||||
shadow.appendChild(button);
|
||||
document.body.appendChild(host);
|
||||
|
||||
let capturedPath = null;
|
||||
|
||||
button.addEventListener('click', (e) => {
|
||||
capturedPath = e.composedPath();
|
||||
});
|
||||
|
||||
const event = new Event('click', { bubbles: true, composed: true });
|
||||
button.dispatchEvent(event);
|
||||
|
||||
testing.expectEqual(7, capturedPath.length);
|
||||
testing.expectEqual(button, capturedPath[0]);
|
||||
testing.expectEqual(shadow, capturedPath[1]);
|
||||
testing.expectEqual(host, capturedPath[2]);
|
||||
testing.expectEqual(document.body, capturedPath[3]);
|
||||
testing.expectEqual(document.documentElement, capturedPath[4]);
|
||||
testing.expectEqual(document, capturedPath[5]);
|
||||
testing.expectEqual(window, capturedPath[6]);
|
||||
|
||||
host.remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -21,6 +21,7 @@ const js = @import("../js/js.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const EventTarget = @import("EventTarget.zig");
|
||||
const Node = @import("Node.zig");
|
||||
const String = @import("../../string.zig").String;
|
||||
|
||||
pub const Event = @This();
|
||||
@@ -28,6 +29,7 @@ pub const Event = @This();
|
||||
_type: Type,
|
||||
_bubbles: bool = false,
|
||||
_cancelable: bool = false,
|
||||
_composed: bool = false,
|
||||
_type_string: String,
|
||||
_target: ?*EventTarget = null,
|
||||
_current_target: ?*EventTarget = null,
|
||||
@@ -36,6 +38,7 @@ _stop_propagation: bool = false,
|
||||
_stop_immediate_propagation: bool = false,
|
||||
_event_phase: EventPhase = .none,
|
||||
_time_stamp: u64 = 0,
|
||||
_needs_retargeting: bool = false,
|
||||
|
||||
pub const EventPhase = enum(u8) {
|
||||
none = 0,
|
||||
@@ -54,6 +57,7 @@ pub const Type = union(enum) {
|
||||
const Options = struct {
|
||||
bubbles: bool = false,
|
||||
cancelable: bool = false,
|
||||
composed: bool = false,
|
||||
};
|
||||
|
||||
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
|
||||
@@ -68,6 +72,7 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
|
||||
._bubbles = opts.bubbles,
|
||||
._time_stamp = time_stamp,
|
||||
._cancelable = opts.cancelable,
|
||||
._composed = opts.composed,
|
||||
._type_string = try String.init(page.arena, typ, .{}),
|
||||
});
|
||||
}
|
||||
@@ -84,6 +89,10 @@ pub fn getCancelable(self: *const Event) bool {
|
||||
return self._cancelable;
|
||||
}
|
||||
|
||||
pub fn getComposed(self: *const Event) bool {
|
||||
return self._composed;
|
||||
}
|
||||
|
||||
pub fn getTarget(self: *const Event) ?*EventTarget {
|
||||
return self._target;
|
||||
}
|
||||
@@ -117,6 +126,68 @@ pub fn getTimeStamp(self: *const Event) u64 {
|
||||
return self._time_stamp;
|
||||
}
|
||||
|
||||
pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
|
||||
// Return empty array if event is not being dispatched
|
||||
if (self._event_phase == .none) {
|
||||
return &.{};
|
||||
}
|
||||
|
||||
// If there's no target, return empty array
|
||||
const target = self._target orelse return &.{};
|
||||
|
||||
// Only nodes have a propagation path
|
||||
const target_node = switch (target._type) {
|
||||
.node => |n| n,
|
||||
else => return &.{},
|
||||
};
|
||||
|
||||
// Build the path by walking up from target
|
||||
var path_len: usize = 0;
|
||||
var path_buffer: [128]*EventTarget = undefined;
|
||||
var stopped_at_shadow_boundary = false;
|
||||
|
||||
var node: ?*Node = target_node;
|
||||
while (node) |n| {
|
||||
if (path_len >= path_buffer.len) {
|
||||
break;
|
||||
}
|
||||
path_buffer[path_len] = n.asEventTarget();
|
||||
path_len += 1;
|
||||
|
||||
// Check if this node is a shadow root
|
||||
if (n._type == .document_fragment) {
|
||||
if (n._type.document_fragment._type == .shadow_root) {
|
||||
const shadow = n._type.document_fragment._type.shadow_root;
|
||||
|
||||
// If event is not composed, stop at shadow boundary
|
||||
if (!self._composed) {
|
||||
stopped_at_shadow_boundary = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Otherwise, jump to the shadow host and continue
|
||||
node = shadow._host.asNode();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
node = n._parent;
|
||||
}
|
||||
|
||||
// Add window at the end (unless we stopped at shadow boundary)
|
||||
if (!stopped_at_shadow_boundary) {
|
||||
if (path_len < path_buffer.len) {
|
||||
path_buffer[path_len] = page.window.asEventTarget();
|
||||
path_len += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Allocate and return the path using call_arena (short-lived)
|
||||
const path = try page.call_arena.alloc(*EventTarget, path_len);
|
||||
@memcpy(path, path_buffer[0..path_len]);
|
||||
return path;
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Event);
|
||||
|
||||
@@ -131,6 +202,7 @@ pub const JsApi = struct {
|
||||
pub const @"type" = bridge.accessor(Event.getType, null, .{});
|
||||
pub const bubbles = bridge.accessor(Event.getBubbles, null, .{});
|
||||
pub const cancelable = bridge.accessor(Event.getCancelable, null, .{});
|
||||
pub const composed = bridge.accessor(Event.getComposed, null, .{});
|
||||
pub const target = bridge.accessor(Event.getTarget, null, .{});
|
||||
pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{});
|
||||
pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{});
|
||||
@@ -139,6 +211,7 @@ pub const JsApi = struct {
|
||||
pub const preventDefault = bridge.function(Event.preventDefault, .{});
|
||||
pub const stopPropagation = bridge.function(Event.stopPropagation, .{});
|
||||
pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{});
|
||||
pub const composedPath = bridge.function(Event.composedPath, .{});
|
||||
|
||||
// Event phase constants
|
||||
pub const NONE = bridge.property(@intFromEnum(EventPhase.none));
|
||||
|
||||
Reference in New Issue
Block a user