mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-16 00:08:59 +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 {
|
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_len: usize = 0;
|
||||||
var path_buffer: [128]*EventTarget = undefined;
|
var path_buffer: [128]*EventTarget = undefined;
|
||||||
|
|
||||||
var node: ?*Node = target;
|
var node: ?*Node = target;
|
||||||
while (node) |n| : (node = n._parent) {
|
while (node) |n| {
|
||||||
if (path_len >= path_buffer.len) break;
|
if (path_len >= path_buffer.len) break;
|
||||||
path_buffer[path_len] = n.asEventTarget();
|
path_buffer[path_len] = n.asEventTarget();
|
||||||
path_len += 1;
|
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
|
// 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) {
|
if (path_len < path_buffer.len) {
|
||||||
path_buffer[path_len] = self.page.window.asEventTarget();
|
path_buffer[path_len] = self.page.window.asEventTarget();
|
||||||
path_len += 1;
|
path_len += 1;
|
||||||
@@ -257,6 +275,12 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
|||||||
was_handled.* = true;
|
was_handled.* = true;
|
||||||
event._current_target = current_target;
|
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) {
|
switch (listener.function) {
|
||||||
.value => |value| try value.call(void, .{event}),
|
.value => |value| try value.call(void, .{event}),
|
||||||
.string => |string| {
|
.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) {
|
if (listener.once) {
|
||||||
self.removeListener(list, listener);
|
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);
|
const result = self.func.castToFunction().call(context.v8_context, js_this, js_args);
|
||||||
if (result == null) {
|
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;
|
return error.JSExecCallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,34 +52,135 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script id="event_composed_escapes_shadow">
|
<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 button = shadow.getElementById('btn');
|
||||||
// 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');
|
let hostCalled = false;
|
||||||
|
let hostTarget = null;
|
||||||
|
|
||||||
// let hostCalled = false;
|
host.addEventListener('click', (e) => {
|
||||||
// let hostTarget = null;
|
hostCalled = true;
|
||||||
|
hostTarget = e.target;
|
||||||
|
});
|
||||||
|
|
||||||
// host.addEventListener('click', (e) => {
|
// With composed:true, event SHOULD escape shadow tree
|
||||||
// hostCalled = true;
|
const event = new Event('click', { bubbles: true, composed: true });
|
||||||
// hostTarget = e.target;
|
button.dispatchEvent(event);
|
||||||
// });
|
|
||||||
|
|
||||||
// // With composed:true, event SHOULD escape shadow tree
|
testing.expectEqual(true, hostCalled);
|
||||||
// const event = new Event('click', { bubbles: true, composed: true });
|
|
||||||
// button.dispatchEvent(event);
|
|
||||||
|
|
||||||
// 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
|
host.remove();
|
||||||
// testing.expectEqual(host, hostTarget);
|
}
|
||||||
|
</script>
|
||||||
// host.remove();
|
|
||||||
// }
|
<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>
|
</script>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ const js = @import("../js/js.zig");
|
|||||||
|
|
||||||
const Page = @import("../Page.zig");
|
const Page = @import("../Page.zig");
|
||||||
const EventTarget = @import("EventTarget.zig");
|
const EventTarget = @import("EventTarget.zig");
|
||||||
|
const Node = @import("Node.zig");
|
||||||
const String = @import("../../string.zig").String;
|
const String = @import("../../string.zig").String;
|
||||||
|
|
||||||
pub const Event = @This();
|
pub const Event = @This();
|
||||||
@@ -28,6 +29,7 @@ pub const Event = @This();
|
|||||||
_type: Type,
|
_type: Type,
|
||||||
_bubbles: bool = false,
|
_bubbles: bool = false,
|
||||||
_cancelable: bool = false,
|
_cancelable: bool = false,
|
||||||
|
_composed: bool = false,
|
||||||
_type_string: String,
|
_type_string: String,
|
||||||
_target: ?*EventTarget = null,
|
_target: ?*EventTarget = null,
|
||||||
_current_target: ?*EventTarget = null,
|
_current_target: ?*EventTarget = null,
|
||||||
@@ -36,6 +38,7 @@ _stop_propagation: bool = false,
|
|||||||
_stop_immediate_propagation: bool = false,
|
_stop_immediate_propagation: bool = false,
|
||||||
_event_phase: EventPhase = .none,
|
_event_phase: EventPhase = .none,
|
||||||
_time_stamp: u64 = 0,
|
_time_stamp: u64 = 0,
|
||||||
|
_needs_retargeting: bool = false,
|
||||||
|
|
||||||
pub const EventPhase = enum(u8) {
|
pub const EventPhase = enum(u8) {
|
||||||
none = 0,
|
none = 0,
|
||||||
@@ -54,6 +57,7 @@ pub const Type = union(enum) {
|
|||||||
const Options = struct {
|
const Options = struct {
|
||||||
bubbles: bool = false,
|
bubbles: bool = false,
|
||||||
cancelable: bool = false,
|
cancelable: bool = false,
|
||||||
|
composed: bool = false,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
|
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,
|
._bubbles = opts.bubbles,
|
||||||
._time_stamp = time_stamp,
|
._time_stamp = time_stamp,
|
||||||
._cancelable = opts.cancelable,
|
._cancelable = opts.cancelable,
|
||||||
|
._composed = opts.composed,
|
||||||
._type_string = try String.init(page.arena, typ, .{}),
|
._type_string = try String.init(page.arena, typ, .{}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -84,6 +89,10 @@ pub fn getCancelable(self: *const Event) bool {
|
|||||||
return self._cancelable;
|
return self._cancelable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn getComposed(self: *const Event) bool {
|
||||||
|
return self._composed;
|
||||||
|
}
|
||||||
|
|
||||||
pub fn getTarget(self: *const Event) ?*EventTarget {
|
pub fn getTarget(self: *const Event) ?*EventTarget {
|
||||||
return self._target;
|
return self._target;
|
||||||
}
|
}
|
||||||
@@ -117,6 +126,68 @@ pub fn getTimeStamp(self: *const Event) u64 {
|
|||||||
return self._time_stamp;
|
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 JsApi = struct {
|
||||||
pub const bridge = js.Bridge(Event);
|
pub const bridge = js.Bridge(Event);
|
||||||
|
|
||||||
@@ -131,6 +202,7 @@ pub const JsApi = struct {
|
|||||||
pub const @"type" = bridge.accessor(Event.getType, null, .{});
|
pub const @"type" = bridge.accessor(Event.getType, null, .{});
|
||||||
pub const bubbles = bridge.accessor(Event.getBubbles, null, .{});
|
pub const bubbles = bridge.accessor(Event.getBubbles, null, .{});
|
||||||
pub const cancelable = bridge.accessor(Event.getCancelable, 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 target = bridge.accessor(Event.getTarget, null, .{});
|
||||||
pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{});
|
pub const currentTarget = bridge.accessor(Event.getCurrentTarget, null, .{});
|
||||||
pub const eventPhase = bridge.accessor(Event.getEventPhase, 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 preventDefault = bridge.function(Event.preventDefault, .{});
|
||||||
pub const stopPropagation = bridge.function(Event.stopPropagation, .{});
|
pub const stopPropagation = bridge.function(Event.stopPropagation, .{});
|
||||||
pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{});
|
pub const stopImmediatePropagation = bridge.function(Event.stopImmediatePropagation, .{});
|
||||||
|
pub const composedPath = bridge.function(Event.composedPath, .{});
|
||||||
|
|
||||||
// Event phase constants
|
// Event phase constants
|
||||||
pub const NONE = bridge.property(@intFromEnum(EventPhase.none));
|
pub const NONE = bridge.property(@intFromEnum(EventPhase.none));
|
||||||
|
|||||||
Reference in New Issue
Block a user