event isTrusted support and better composedPath for shadowroots

This commit is contained in:
Karl Seguin
2025-12-26 08:45:20 +08:00
parent b379b775f9
commit 25dbac9945
32 changed files with 357 additions and 60 deletions

View File

@@ -83,12 +83,14 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
var node = gop.value_ptr.*.first; var node = gop.value_ptr.*.first;
while (node) |n| { while (node) |n| {
const listener: *Listener = @alignCast(@fieldParentPtr("node", n)); const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
const is_duplicate = switch (callback) { if (listener.typ.eqlSlice(typ)) {
.object => |obj| listener.function.eqlObject(obj), const is_duplicate = switch (callback) {
.function => |func| listener.function.eqlFunction(func), .object => |obj| listener.function.eqlObject(obj),
}; .function => |func| listener.function.eqlFunction(func),
if (is_duplicate and listener.capture == opts.capture) { };
return; if (is_duplicate and listener.capture == opts.capture) {
return;
}
} }
node = n.next; node = n.next;
} }
@@ -132,6 +134,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
} }
event._target = target; event._target = target;
event._dispatch_target = target; // Store original target for composedPath()
var was_handled = false; var was_handled = false;
defer if (was_handled) { defer if (was_handled) {
@@ -173,6 +176,7 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
if (comptime opts.inject_target) { if (comptime opts.inject_target) {
event._target = target; event._target = target;
event._dispatch_target = target; // Store original target for composedPath()
} }
var was_dispatched = false; var was_dispatched = false;

View File

@@ -503,7 +503,7 @@ pub fn documentIsLoaded(self: *Page) void {
} }
pub fn _documentIsLoaded(self: *Page) !void { pub fn _documentIsLoaded(self: *Page) !void {
const event = try Event.init("DOMContentLoaded", .{ .bubbles = true }, self); const event = try Event.initTrusted("DOMContentLoaded", .{ .bubbles = true }, self);
try self._event_manager.dispatch( try self._event_manager.dispatch(
self.document.asEventTarget(), self.document.asEventTarget(),
event, event,
@@ -549,7 +549,7 @@ fn _documentIsComplete(self: *Page) !void {
self.document._ready_state = .complete; self.document._ready_state = .complete;
// dispatch window.load event // dispatch window.load event
const event = try Event.init("load", .{}, self); const event = try Event.initTrusted("load", .{}, self);
// this event is weird, it's dispatched directly on the window, but // this event is weird, it's dispatched directly on the window, but
// with the document as the target // with the document as the target
event._target = self.document.asEventTarget(); event._target = self.document.asEventTarget();
@@ -560,7 +560,7 @@ fn _documentIsComplete(self: *Page) !void {
.{ .inject_target = false, .context = "page load" }, .{ .inject_target = false, .context = "page load" },
); );
const pageshow_event = try PageTransitionEvent.init("pageshow", .{}, self); const pageshow_event = try PageTransitionEvent.initTrusted("pageshow", .{}, self);
try self._event_manager.dispatchWithFunction( try self._event_manager.dispatchWithFunction(
self.window.asEventTarget(), self.window.asEventTarget(),
pageshow_event.asEvent(), pageshow_event.asEvent(),
@@ -1174,7 +1174,7 @@ pub fn deliverSlotchangeEvents(self: *Page) void {
self._slots_pending_slotchange.clearRetainingCapacity(); self._slots_pending_slotchange.clearRetainingCapacity();
for (slots) |slot| { for (slots) |slot| {
const event = Event.init("slotchange", .{ .bubbles = true }, self) catch |err| { const event = Event.initTrusted("slotchange", .{ .bubbles = true }, self) catch |err| {
log.err(.page, "deliverSlotchange.init", .{ .err = err }); log.err(.page, "deliverSlotchange.init", .{ .err = err });
continue; continue;
}; };
@@ -2419,6 +2419,7 @@ pub fn triggerMouseClick(self: *Page, x: f64, y: f64) !void {
const event = try @import("webapi/event/MouseEvent.zig").init("click", .{ const event = try @import("webapi/event/MouseEvent.zig").init("click", .{
.bubbles = true, .bubbles = true,
.cancelable = true, .cancelable = true,
.composed = true,
.clientX = x, .clientX = x,
.clientY = y, .clientY = y,
}, self); }, self);

View File

@@ -833,7 +833,7 @@ pub const Script = struct {
const cb = cb_ orelse return; const cb = cb_ orelse return;
const Event = @import("webapi/Event.zig"); const Event = @import("webapi/Event.zig");
const event = Event.init(typ, .{}, page) catch |err| { const event = Event.initTrusted(typ, .{}, page) catch |err| {
log.warn(.js, "script internal callback", .{ log.warn(.js, "script internal callback", .{
.url = self.url, .url = self.url,
.type = typ, .type = typ,

View File

@@ -88,3 +88,18 @@
testing.expectEqual(true, isKeyPress); testing.expectEqual(true, isKeyPress);
</script> </script>
<script id=isTrusted>
// Test isTrusted on KeyboardEvent
let keyEvent = new KeyboardEvent('keydown', {key: 'a'});
testing.expectEqual(false, keyEvent.isTrusted);
// Test isTrusted on dispatched KeyboardEvent
let keyIsTrusted = null;
document.addEventListener('keytest', (e) => {
keyIsTrusted = e.isTrusted;
testing.expectEqual(true, e instanceof KeyboardEvent);
});
document.dispatchEvent(new KeyboardEvent('keytest', {key: 'b'}));
testing.expectEqual(false, keyIsTrusted);
</script>

View File

@@ -35,3 +35,18 @@
testing.expectEqual('click', evt.type); testing.expectEqual('click', evt.type);
testing.expectEqual(true, evt instanceof MouseEvent); testing.expectEqual(true, evt instanceof MouseEvent);
</script> </script>
<script id=isTrusted>
// Test isTrusted on MouseEvent
let mouseEvent = new MouseEvent('click');
testing.expectEqual(false, mouseEvent.isTrusted);
// Test isTrusted on dispatched MouseEvent
let mouseIsTrusted = null;
document.addEventListener('mousetest', (e) => {
mouseIsTrusted = e.isTrusted;
testing.expectEqual(true, e instanceof MouseEvent);
});
document.dispatchEvent(new MouseEvent('mousetest'));
testing.expectEqual(false, mouseIsTrusted);
</script>

View File

@@ -612,3 +612,21 @@
content.dispatchEvent(new Event('he2')); content.dispatchEvent(new Event('he2'));
} }
</script> </script>
<script id=isTrusted>
// Test isTrusted property on generic Event
let untrustedEvent = new Event('test');
testing.expectEqual(false, untrustedEvent.isTrusted);
// Test isTrusted on dispatched events
let isTrustedValue = null;
document.addEventListener('trusttest', (e) => {
isTrustedValue = e.isTrusted;
});
document.dispatchEvent(new Event('trusttest'));
testing.expectEqual(false, isTrustedValue);
// Test isTrusted with bubbling events
let bubbledEvent = new Event('bubble', {bubbles: true});
testing.expectEqual(false, bubbledEvent.isTrusted);
</script>

View File

@@ -27,7 +27,7 @@
testing.expectEqual(true, evt.bubbles); testing.expectEqual(true, evt.bubbles);
testing.expectEqual(true, evt.cancelable); testing.expectEqual(true, evt.cancelable);
testing.expectEqual(true, evt.defaultPrevented); testing.expectEqual(true, evt.defaultPrevented);
testing.expectEqual(true, evt.isTrusted); testing.expectEqual(false, evt.isTrusted);
testing.expectEqual(true, evt.timeStamp >= Math.floor(startTime)); testing.expectEqual(true, evt.timeStamp >= Math.floor(startTime));
</script> </script>

View File

@@ -184,3 +184,100 @@
host.remove(); host.remove();
} }
</script> </script>
<script id="composedPath_open_shadow_from_host">
{
// Test that open shadow root exposes internals in composedPath
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;
host.addEventListener('click', (e) => {
capturedPath = e.composedPath();
});
const event = new Event('click', { bubbles: true, composed: true });
button.dispatchEvent(event);
// Open shadow: external listener should see shadow internals
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 id="composedPath_closed_shadow_from_host">
{
// Test that closed shadow root hides internals in composedPath
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'closed' });
const button = document.createElement('button');
shadow.appendChild(button);
document.body.appendChild(host);
let capturedPath = null;
host.addEventListener('click', (e) => {
capturedPath = e.composedPath();
});
const event = new Event('click', { bubbles: true, composed: true });
button.dispatchEvent(event);
// Closed shadow: external listener should NOT see shadow internals
testing.expectEqual(5, capturedPath.length);
testing.expectEqual(host, capturedPath[0]);
testing.expectEqual(document.body, capturedPath[1]);
testing.expectEqual(document.documentElement, capturedPath[2]);
testing.expectEqual(document, capturedPath[3]);
testing.expectEqual(window, capturedPath[4]);
host.remove();
}
</script>
<script id="composedPath_closed_shadow_from_inside">
{
// Test that closed shadow root still exposes internals to internal listeners
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'closed' });
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);
// Inside the shadow: should see full path including shadow internals
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>

View File

@@ -80,7 +80,7 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
} }
// Dispatch abort event // Dispatch abort event
const event = try Event.init("abort", .{}, page); const event = try Event.initTrusted("abort", .{}, page);
try page._event_manager.dispatchWithFunction( try page._event_manager.dispatchWithFunction(
self.asEventTarget(), self.asEventTarget(),
event, event,

View File

@@ -251,7 +251,7 @@ pub fn createTextNode(_: *const Document, data: []const u8, page: *Page) !*Node
pub fn createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node { pub fn createCDATASection(self: *const Document, data: []const u8, page: *Page) !*Node {
switch (self._type) { switch (self._type) {
.html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument .html => return error.NotSupported, // cannot create a CDataSection in an HTMLDocument
.xml => return page.createCDATASection(data), .xml => return page.createCDATASection(data),
.generic => return page.createCDATASection(data), .generic => return page.createCDATASection(data),
} }
@@ -570,7 +570,7 @@ pub fn getChildElementCount(self: *Document) u32 {
return i; return i;
} }
pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object { pub fn getAdoptedStyleSheets(self: *Document, page: *Page) !js.Object {
if (self._adopted_style_sheets) |ass| { if (self._adopted_style_sheets) |ass| {
return ass; return ass;
} }

View File

@@ -596,7 +596,7 @@ pub fn focus(self: *Element, page: *Page) !void {
return; return;
} }
const blur_event = try Event.init("blur", null, page); const blur_event = try Event.initTrusted("blur", null, page);
try page._event_manager.dispatch(old.asEventTarget(), blur_event); try page._event_manager.dispatch(old.asEventTarget(), blur_event);
} }
@@ -604,7 +604,7 @@ pub fn focus(self: *Element, page: *Page) !void {
page.document._active_element = self; page.document._active_element = self;
} }
const focus_event = try Event.init("focus", null, page); const focus_event = try Event.initTrusted("focus", null, page);
try page._event_manager.dispatch(self.asEventTarget(), focus_event); try page._event_manager.dispatch(self.asEventTarget(), focus_event);
} }
@@ -614,7 +614,7 @@ pub fn blur(self: *Element, page: *Page) !void {
page.document._active_element = null; page.document._active_element = null;
const Event = @import("Event.zig"); const Event = @import("Event.zig");
const blur_event = try Event.init("blur", null, page); const blur_event = try Event.initTrusted("blur", null, page);
try page._event_manager.dispatch(self.asEventTarget(), blur_event); try page._event_manager.dispatch(self.asEventTarget(), blur_event);
} }

View File

@@ -35,12 +35,14 @@ _composed: bool = false,
_type_string: String, _type_string: String,
_target: ?*EventTarget = null, _target: ?*EventTarget = null,
_current_target: ?*EventTarget = null, _current_target: ?*EventTarget = null,
_dispatch_target: ?*EventTarget = null, // Original target for composedPath()
_prevent_default: bool = false, _prevent_default: bool = false,
_stop_propagation: bool = false, _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, _needs_retargeting: bool = false,
_isTrusted: bool = false,
pub const EventPhase = enum(u8) { pub const EventPhase = enum(u8) {
none = 0, none = 0,
@@ -68,14 +70,22 @@ pub const Options = struct {
composed: bool = false, composed: bool = false,
}; };
pub fn initTrusted(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
return initWithTrusted(typ, opts_, true, page);
}
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event { pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
return initWithTrusted(typ, opts_, false, page);
}
fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page) !*Event {
const opts = opts_ orelse Options{}; const opts = opts_ orelse Options{};
// Round to 2ms for privacy (browsers do this) // Round to 2ms for privacy (browsers do this)
const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic); const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic);
const time_stamp = (raw_timestamp / 2) * 2; const time_stamp = (raw_timestamp / 2) * 2;
return page._factory.create(Event{ const event = try page._factory.create(Event{
._type = .generic, ._type = .generic,
._bubbles = opts.bubbles, ._bubbles = opts.bubbles,
._time_stamp = time_stamp, ._time_stamp = time_stamp,
@@ -83,6 +93,21 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event {
._composed = opts.composed, ._composed = opts.composed,
._type_string = try String.init(page.arena, typ, .{}), ._type_string = try String.init(page.arena, typ, .{}),
}); });
event._isTrusted = trusted;
return event;
}
pub fn initEvent(
self: *Event,
event_string: []const u8,
bubbles: ?bool,
cancelable: ?bool,
page: *Page,
) !void {
self._type_string = try String.init(page.arena, event_string, .{});
self._bubbles = bubbles orelse false;
self._cancelable = cancelable orelse false;
} }
pub fn as(self: *Event, comptime T: type) *T { pub fn as(self: *Event, comptime T: type) *T {
@@ -159,14 +184,27 @@ pub fn getTimeStamp(self: *const Event) u64 {
return self._time_stamp; return self._time_stamp;
} }
pub fn setTrusted(self: *Event) void {
self._isTrusted = true;
}
pub fn setUntrusted(self: *Event) void {
self._isTrusted = false;
}
pub fn getIsTrusted(self: *const Event) bool {
return self._isTrusted;
}
pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget { pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
// Return empty array if event is not being dispatched // Return empty array if event is not being dispatched
if (self._event_phase == .none) { if (self._event_phase == .none) {
return &.{}; return &.{};
} }
// If there's no target, return empty array // Use dispatch_target (original target) if available, otherwise fall back to target
const target = self._target orelse return &.{}; // This is important because _target gets retargeted during event dispatch
const target = self._dispatch_target orelse self._target orelse return &.{};
// Only nodes have a propagation path // Only nodes have a propagation path
const target_node = switch (target._type) { const target_node = switch (target._type) {
@@ -179,6 +217,9 @@ pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
var path_buffer: [128]*EventTarget = undefined; var path_buffer: [128]*EventTarget = undefined;
var stopped_at_shadow_boundary = false; var stopped_at_shadow_boundary = false;
// Track closed shadow boundaries (position in path and host position)
var closed_shadow_boundary: ?struct { shadow_end: usize, host_start: usize } = null;
var node: ?*Node = target_node; var node: ?*Node = target_node;
while (node) |n| { while (node) |n| {
if (path_len >= path_buffer.len) { if (path_len >= path_buffer.len) {
@@ -198,7 +239,17 @@ pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
break; break;
} }
// Otherwise, jump to the shadow host and continue // Track the first closed shadow boundary we encounter
if (shadow._mode == .closed and closed_shadow_boundary == null) {
// Mark where the shadow root is in the path
// The next element will be the host
closed_shadow_boundary = .{
.shadow_end = path_len - 1, // index of shadow root
.host_start = path_len, // index where host will be
};
}
// Jump to the shadow host and continue
node = shadow._host.asNode(); node = shadow._host.asNode();
continue; continue;
} }
@@ -215,9 +266,40 @@ pub fn composedPath(self: *Event, page: *Page) ![]const *EventTarget {
} }
} }
// Allocate and return the path using call_arena (short-lived) // Determine visible path based on current_target and closed shadow boundaries
const path = try page.call_arena.alloc(*EventTarget, path_len); var visible_start_index: usize = 0;
@memcpy(path, path_buffer[0..path_len]);
if (closed_shadow_boundary) |boundary| {
// Check if current_target is outside the closed shadow
// If current_target is null or is at/after the host position, hide shadow internals
const current_target = self._current_target;
if (current_target) |ct| {
// Find current_target in the path
var ct_index: ?usize = null;
for (path_buffer[0..path_len], 0..) |elem, i| {
if (elem == ct) {
ct_index = i;
break;
}
}
// If current_target is at or after the host (outside the closed shadow),
// hide everything from target up to the host
if (ct_index) |idx| {
if (idx >= boundary.host_start) {
visible_start_index = boundary.host_start;
}
}
}
}
// Calculate the visible portion of the path
const visible_path_len = if (path_len > visible_start_index) path_len - visible_start_index else 0;
// Allocate and return the visible path using call_arena (short-lived)
const path = try page.call_arena.alloc(*EventTarget, visible_path_len);
@memcpy(path, path_buffer[visible_start_index..path_len]);
return path; return path;
} }
@@ -257,16 +339,21 @@ pub fn inheritOptions(comptime T: type, comptime additions: anytype) type {
}); });
} }
pub fn populatePrototypes(self: anytype, opts: anytype) void { pub fn populatePrototypes(self: anytype, opts: anytype, trusted: bool) void {
const T = @TypeOf(self.*); const T = @TypeOf(self.*);
if (@hasField(T, "_proto")) { if (@hasField(T, "_proto")) {
populatePrototypes(self._proto, opts); populatePrototypes(self._proto, opts, trusted);
} }
if (@hasDecl(T, "populateFromOptions")) { if (@hasDecl(T, "populateFromOptions")) {
T.populateFromOptions(self, opts); T.populateFromOptions(self, opts);
} }
// Set isTrusted at the Event level (base of prototype chain)
if (T == Event or @hasField(T, "_isTrusted")) {
self._isTrusted = trusted;
}
} }
pub const JsApi = struct { pub const JsApi = struct {
@@ -289,10 +376,12 @@ pub const JsApi = struct {
pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{}); pub const eventPhase = bridge.accessor(Event.getEventPhase, null, .{});
pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{}); pub const defaultPrevented = bridge.accessor(Event.getDefaultPrevented, null, .{});
pub const timeStamp = bridge.accessor(Event.getTimeStamp, null, .{}); pub const timeStamp = bridge.accessor(Event.getTimeStamp, null, .{});
pub const isTrusted = bridge.accessor(Event.getIsTrusted, null, .{});
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, .{}); pub const composedPath = bridge.function(Event.composedPath, .{});
pub const initEvent = bridge.function(Event.initEvent, .{});
// Event phase constants // Event phase constants
pub const NONE = bridge.property(@intFromEnum(EventPhase.none)); pub const NONE = bridge.property(@intFromEnum(EventPhase.none));

View File

@@ -79,7 +79,7 @@ fn goInner(delta: i32, page: *Page) !void {
if (entry._url) |url| { if (entry._url) |url| {
if (try page.isSameOrigin(url)) { if (try page.isSameOrigin(url)) {
const event = try PopStateEvent.init("popstate", .{ .state = entry._state.value }, page); const event = try PopStateEvent.initTrusted("popstate", .{ .state = entry._state.value }, page);
try page._event_manager.dispatchWithFunction( try page._event_manager.dispatchWithFunction(
page.window.asEventTarget(), page.window.asEventTarget(),

View File

@@ -129,7 +129,7 @@ const PostMessageCallback = struct {
return null; return null;
} }
const event = MessageEvent.init("message", .{ const event = MessageEvent.initTrusted("message", .{
.data = self.message, .data = self.message,
.origin = "", .origin = "",
.source = null, .source = null,

View File

@@ -33,7 +33,7 @@ pub fn asAbstractRange(self: *Range) *AbstractRange {
} }
pub fn init(page: *Page) !*Range { pub fn init(page: *Page) !*Range {
return page._factory.abstractRange(Range{._proto = undefined}, page); return page._factory.abstractRange(Range{ ._proto = undefined }, page);
} }
pub fn setStart(self: *Range, node: *Node, offset: u32) !void { pub fn setStart(self: *Range, node: *Node, offset: u32) !void {
@@ -106,7 +106,7 @@ pub fn collapse(self: *Range, to_start: ?bool) void {
} }
pub fn cloneRange(self: *const Range, page: *Page) !*Range { pub fn cloneRange(self: *const Range, page: *Page) !*Range {
const clone = try page._factory.abstractRange(Range{._proto = undefined}, page); const clone = try page._factory.abstractRange(Range{ ._proto = undefined }, page);
clone._proto._end_offset = self._proto._end_offset; clone._proto._end_offset = self._proto._end_offset;
clone._proto._start_offset = self._proto._start_offset; clone._proto._start_offset = self._proto._start_offset;
clone._proto._end_container = self._proto._end_container; clone._proto._end_container = self._proto._end_container;

View File

@@ -270,7 +270,7 @@ pub fn cancelIdleCallback(self: *Window, id: u32) void {
} }
pub fn reportError(self: *Window, err: js.Object, page: *Page) !void { pub fn reportError(self: *Window, err: js.Object, page: *Page) !void {
const error_event = try ErrorEvent.init("error", .{ const error_event = try ErrorEvent.initTrusted("error", .{
.@"error" = err, .@"error" = err,
.message = err.toString() catch "Unknown error", .message = err.toString() catch "Unknown error",
.bubbles = false, .bubbles = false,
@@ -410,7 +410,7 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
return null; return null;
} }
const event = try Event.init("scroll", .{ .bubbles = true }, p); const event = try Event.initTrusted("scroll", .{ .bubbles = true }, p);
try p._event_manager.dispatch(p.document.asEventTarget(), event); try p._event_manager.dispatch(p.document.asEventTarget(), event);
pos.state = .end; pos.state = .end;
@@ -437,7 +437,7 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
.end => {}, .end => {},
.done => return null, .done => return null,
} }
const event = try Event.init("scrollend", .{ .bubbles = true }, p); const event = try Event.initTrusted("scrollend", .{ .bubbles = true }, p);
try p._event_manager.dispatch(p.document.asEventTarget(), event); try p._event_manager.dispatch(p.document.asEventTarget(), event);
pos.state = .done; pos.state = .done;
@@ -586,7 +586,7 @@ const PostMessageCallback = struct {
const self: *PostMessageCallback = @ptrCast(@alignCast(ctx)); const self: *PostMessageCallback = @ptrCast(@alignCast(ctx));
defer self.deinit(); defer self.deinit();
const message_event = try MessageEvent.init("message", .{ const message_event = try MessageEvent.initTrusted("message", .{
.data = self.message, .data = self.message,
.origin = self.origin, .origin = self.origin,
.source = self.window, .source = self.window,

View File

@@ -306,6 +306,7 @@ pub fn click(self: *HtmlElement, page: *Page) !void {
const event = try @import("../event/MouseEvent.zig").init("click", .{ const event = try @import("../event/MouseEvent.zig").init("click", .{
.bubbles = true, .bubbles = true,
.cancelable = true, .cancelable = true,
.composed = true,
.clientX = 0, .clientX = 0,
.clientY = 0, .clientY = 0,
}, page); }, page);

View File

@@ -40,7 +40,7 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent {
._data = if (opts.data) |str| try page.dupeString(str) else "", ._data = if (opts.data) |str| try page.dupeString(str) else "",
}); });
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, false);
return event; return event;
} }

View File

@@ -49,23 +49,23 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CustomEvent {
}, },
); );
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, false);
return event; return event;
} }
pub fn initCustomEvent( pub fn initCustomEvent(
self: *CustomEvent, self: *CustomEvent,
event_string: []const u8, event_string: []const u8,
bubbles: bool, bubbles: ?bool,
cancelable: bool, cancelable: ?bool,
detail_: ?js.Object, detail_: ?js.Object,
page: *Page, page: *Page,
) !void { ) !void {
// This function can only be called after the constructor has called. // This function can only be called after the constructor has called.
// So we assume proto is initialized already by constructor. // So we assume proto is initialized already by constructor.
self._proto._type_string = try String.init(page.arena, event_string, .{}); self._proto._type_string = try String.init(page.arena, event_string, .{});
self._proto._bubbles = bubbles; self._proto._bubbles = bubbles orelse false;
self._proto._cancelable = cancelable; self._proto._cancelable = cancelable orelse false;
// Detail is stored separately. // Detail is stored separately.
if (detail_) |detail| { if (detail_) |detail| {
self._detail = try detail.persist(); self._detail = try detail.persist();

View File

@@ -44,6 +44,14 @@ pub const ErrorEventOptions = struct {
const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions); const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent { pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent {
return initWithTrusted(typ, opts_, false, page);
}
pub fn initTrusted(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent {
return initWithTrusted(typ, opts_, true, page);
}
fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page) !*ErrorEvent {
const arena = page.arena; const arena = page.arena;
const opts = opts_ orelse Options{}; const opts = opts_ orelse Options{};
@@ -60,7 +68,7 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent {
}, },
); );
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, trusted);
return event; return event;
} }

View File

@@ -182,7 +182,15 @@ const Options = Event.inheritOptions(
KeyboardEventOptions, KeyboardEventOptions,
); );
pub fn initTrusted(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent {
return initWithTrusted(typ, _opts, true, page);
}
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent { pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent {
return initWithTrusted(typ, _opts, false, page);
}
fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page) !*KeyboardEvent {
const opts = _opts orelse Options{}; const opts = _opts orelse Options{};
const event = try page._factory.uiEvent( const event = try page._factory.uiEvent(
@@ -201,7 +209,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*KeyboardEvent {
}, },
); );
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, trusted);
return event; return event;
} }

View File

@@ -38,6 +38,14 @@ const MessageEventOptions = struct {
const Options = Event.inheritOptions(MessageEvent, MessageEventOptions); const Options = Event.inheritOptions(MessageEvent, MessageEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent { pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent {
return initWithTrusted(typ, opts_, false, page);
}
pub fn initTrusted(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent {
return initWithTrusted(typ, opts_, true, page);
}
fn initWithTrusted(typ: []const u8, opts_: ?Options, trusted: bool, page: *Page) !*MessageEvent {
const opts = opts_ orelse Options{}; const opts = opts_ orelse Options{};
const event = try page._factory.event( const event = try page._factory.event(
@@ -50,7 +58,7 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent {
}, },
); );
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, trusted);
return event; return event;
} }

View File

@@ -88,7 +88,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent {
}, },
); );
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, false);
return event; return event;
} }

View File

@@ -40,9 +40,18 @@ const Options = Event.inheritOptions(
NavigationCurrentEntryChangeEventOptions, NavigationCurrentEntryChangeEventOptions,
); );
pub fn init( pub fn init(typ: []const u8, opts: Options, page: *Page) !*NavigationCurrentEntryChangeEvent {
return initWithTrusted(typ, opts, false, page);
}
pub fn initTrusted(typ: []const u8, opts: Options, page: *Page) !*NavigationCurrentEntryChangeEvent {
return initWithTrusted(typ, opts, true, page);
}
fn initWithTrusted(
typ: []const u8, typ: []const u8,
opts: Options, opts: Options,
trusted: bool,
page: *Page, page: *Page,
) !*NavigationCurrentEntryChangeEvent { ) !*NavigationCurrentEntryChangeEvent {
const navigation_type = if (opts.navigationType) |nav_type_str| const navigation_type = if (opts.navigationType) |nav_type_str|
@@ -59,7 +68,7 @@ pub fn init(
}, },
); );
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, trusted);
return event; return event;
} }

View File

@@ -35,6 +35,14 @@ const PageTransitionEventOptions = struct {
const Options = Event.inheritOptions(PageTransitionEvent, PageTransitionEventOptions); const Options = Event.inheritOptions(PageTransitionEvent, PageTransitionEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PageTransitionEvent { pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PageTransitionEvent {
return initWithTrusted(typ, _opts, false, page);
}
pub fn initTrusted(typ: []const u8, _opts: ?Options, page: *Page) !*PageTransitionEvent {
return initWithTrusted(typ, _opts, true, page);
}
fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page) !*PageTransitionEvent {
const opts = _opts orelse Options{}; const opts = _opts orelse Options{};
const event = try page._factory.event( const event = try page._factory.event(
@@ -45,7 +53,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PageTransitionEvent
}, },
); );
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, trusted);
return event; return event;
} }

View File

@@ -35,6 +35,14 @@ const PopStateEventOptions = struct {
const Options = Event.inheritOptions(PopStateEvent, PopStateEventOptions); const Options = Event.inheritOptions(PopStateEvent, PopStateEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PopStateEvent { pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PopStateEvent {
return initWithTrusted(typ, _opts, false, page);
}
pub fn initTrusted(typ: []const u8, _opts: ?Options, page: *Page) !*PopStateEvent {
return initWithTrusted(typ, _opts, true, page);
}
fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page) !*PopStateEvent {
const opts = _opts orelse Options{}; const opts = _opts orelse Options{};
const event = try page._factory.event( const event = try page._factory.event(
@@ -45,7 +53,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PopStateEvent {
}, },
); );
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, trusted);
return event; return event;
} }

View File

@@ -34,6 +34,14 @@ const ProgressEventOptions = struct {
const Options = Event.inheritOptions(ProgressEvent, ProgressEventOptions); const Options = Event.inheritOptions(ProgressEvent, ProgressEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*ProgressEvent { pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*ProgressEvent {
return initWithTrusted(typ, _opts, false, page);
}
pub fn initTrusted(typ: []const u8, _opts: ?Options, page: *Page) !*ProgressEvent {
return initWithTrusted(typ, _opts, true, page);
}
fn initWithTrusted(typ: []const u8, _opts: ?Options, trusted: bool, page: *Page) !*ProgressEvent {
const opts = _opts orelse Options{}; const opts = _opts orelse Options{};
const event = try page._factory.event( const event = try page._factory.event(
@@ -45,7 +53,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*ProgressEvent {
}, },
); );
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, trusted);
return event; return event;
} }

View File

@@ -57,7 +57,7 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
}, },
); );
Event.populatePrototypes(event, opts); Event.populatePrototypes(event, opts, false);
return event; return event;
} }

View File

@@ -201,7 +201,7 @@ pub fn pushEntry(
if (previous) |prev| { if (previous) |prev| {
if (dispatch) { if (dispatch) {
const event = try NavigationCurrentEntryChangeEvent.init( const event = try NavigationCurrentEntryChangeEvent.initTrusted(
"currententrychange", "currententrychange",
.{ .from = prev, .navigationType = @tagName(.push) }, .{ .from = prev, .navigationType = @tagName(.push) },
page, page,
@@ -240,7 +240,7 @@ pub fn replaceEntry(
self._entries.items[self._index] = entry; self._entries.items[self._index] = entry;
if (dispatch) { if (dispatch) {
const event = try NavigationCurrentEntryChangeEvent.init( const event = try NavigationCurrentEntryChangeEvent.initTrusted(
"currententrychange", "currententrychange",
.{ .from = previous, .navigationType = @tagName(.replace) }, .{ .from = previous, .navigationType = @tagName(.replace) },
page, page,
@@ -324,7 +324,7 @@ pub fn navigateInner(
} }
// If we haven't navigated off, let us fire off an a currententrychange. // If we haven't navigated off, let us fire off an a currententrychange.
const event = try NavigationCurrentEntryChangeEvent.init( const event = try NavigationCurrentEntryChangeEvent.initTrusted(
"currententrychange", "currententrychange",
.{ .from = previous, .navigationType = @tagName(kind) }, .{ .from = previous, .navigationType = @tagName(kind) },
page, page,
@@ -363,7 +363,7 @@ pub fn reload(self: *Navigation, _opts: ?ReloadOptions, page: *Page) !Navigation
const previous = entry; const previous = entry;
entry._state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone }; entry._state = .{ .source = .navigation, .value = state.toJson(arena) catch return error.DataClone };
const event = try NavigationCurrentEntryChangeEvent.init( const event = try NavigationCurrentEntryChangeEvent.initTrusted(
"currententrychange", "currententrychange",
.{ .from = previous, .navigationType = @tagName(.reload) }, .{ .from = previous, .navigationType = @tagName(.reload) },
page, page,
@@ -405,7 +405,7 @@ pub fn updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions,
.value = options.state.toJson(arena) catch return error.DataClone, .value = options.state.toJson(arena) catch return error.DataClone,
}; };
const event = try NavigationCurrentEntryChangeEvent.init( const event = try NavigationCurrentEntryChangeEvent.initTrusted(
"currententrychange", "currententrychange",
.{ .from = previous, .navigationType = null }, .{ .from = previous, .navigationType = null },
page, page,

View File

@@ -416,7 +416,7 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void {
self._ready_state = state; self._ready_state = state;
const event = try Event.init("readystatechange", .{}, page); const event = try Event.initTrusted("readystatechange", .{}, page);
try page._event_manager.dispatchWithFunction( try page._event_manager.dispatchWithFunction(
self.asEventTarget(), self.asEventTarget(),
event, event,

View File

@@ -57,7 +57,7 @@ pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchT
}; };
const progress = progress_ orelse Progress{}; const progress = progress_ orelse Progress{};
const event = try ProgressEvent.init( const event = try ProgressEvent.initTrusted(
typ, typ,
.{ .total = progress.total, .loaded = progress.loaded }, .{ .total = progress.total, .loaded = progress.loaded },
page, page,

View File

@@ -60,7 +60,7 @@ fn dispatchKeyEvent(cmd: anytype) !void {
const page = bc.session.currentPage() orelse return; const page = bc.session.currentPage() orelse return;
const KeyboardEvent = @import("../../browser/webapi/event/KeyboardEvent.zig"); const KeyboardEvent = @import("../../browser/webapi/event/KeyboardEvent.zig");
const keyboard_event = try KeyboardEvent.init("keydown", .{ const keyboard_event = try KeyboardEvent.initTrusted("keydown", .{
.key = params.key, .key = params.key,
.code = params.code, .code = params.code,
.altKey = params.modifiers & 1 == 1, .altKey = params.modifiers & 1 == 1,