diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 4802d9fe7..7cffc2d79 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -28,6 +28,7 @@ const Node = @import("webapi/Node.zig"); const Event = @import("webapi/Event.zig"); const EventTarget = @import("webapi/EventTarget.zig"); const Element = @import("webapi/Element.zig"); +const ShadowRoot = @import("webapi/ShadowRoot.zig"); const log = lp.log; const Allocator = std.mem.Allocator; @@ -111,10 +112,27 @@ pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, co switch (target._type) { .node => |node| try self.dispatchNode(node, event, opts), + .xhr => |xhr| try self.dispatchDirect(target, event, xhr.inlineHandler(event._type_string), .{ .context = "dispatch" }), + .window => |w| try self.dispatchDirect(target, event, windowInlineHandler(w, event._type_string), .{ .context = "dispatch" }), else => try self.dispatchDirect(target, event, null, .{ .context = "dispatch" }), } } +// Resolves the Window's property event handler for the given event type. +fn windowInlineHandler(window: *@import("webapi/Window.zig"), typ: lp.String) ?js.Function.Global { + const global_event_handlers = @import("webapi/global_event_handlers.zig"); + const handler_type = global_event_handlers.fromEventType(typ.str()) orelse return null; + return switch (handler_type) { + .onerror => window._on_error, + .onload => window._on_load, + .onblur => window._on_blur, + .onfocus => window._on_focus, + .onresize => window._on_resize, + .onscroll => window._on_scroll, + else => null, + }; +} + // There are a lot of events that can be attached via addEventListener or as // a property, like the XHR events, or window.onload. You might think that the // property is just a shortcut for calling addEventListener, but they are distinct. @@ -144,8 +162,6 @@ pub fn hasDirectListeners(self: *EventManager, target: *EventTarget, typ: []cons } fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts: DispatchOpts) !void { - const ShadowRoot = @import("webapi/ShadowRoot.zig"); - { const et = target.asEventTarget(); event._target = et; @@ -284,6 +300,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts was_handled = true; event._current_target = target_et; + const prev_current_event = window._current_event; + window._current_event = currentEventForTarget(target_et, event); + defer window._current_event = prev_current_event; + // Inline handlers (e.g. onclick property) follow the same "report, // don't propagate" rule as addEventListener listeners — see Listener.run. var caught: js.TryCatch.Caught = undefined; @@ -331,6 +351,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts was_handled = true; event._current_target = current_target; + const prev_current_event = window._current_event; + window._current_event = currentEventForTarget(current_target, event); + defer window._current_event = prev_current_event; + const original_target = event._target; if (event._needs_retargeting) { event._target = getAdjustedTarget(original_target, current_target); @@ -360,6 +384,10 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts } } +fn currentEventForTarget(target: *EventTarget, event: *Event) ?*Event { + return if (rootIsShadowRoot(target)) null else event; +} + const DispatchPhaseOpts = struct { capture_only: ?bool = null, apply_ignore: bool = false, @@ -376,6 +404,11 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe const frame = self.frame; const base = &self.base; + const window = frame.window; + const prev_current_event = window._current_event; + window._current_event = currentEventForTarget(current_target, event); + defer window._current_event = prev_current_event; + // Track dispatch depth for deferred removal base.dispatch_depth += 1; defer { @@ -477,6 +510,17 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j // Look up the inline handler for this target const html_element = switch (target._type) { .node => |n| n.is(Element.Html) orelse return null, + // The Window stores its event handlers in dedicated fields; an event + // propagating to the window must fire them too. + .window => |w| return switch (handler_type) { + .onerror => w._on_error, + .onload => w._on_load, + .onblur => w._on_blur, + .onfocus => w._on_focus, + .onresize => w._on_resize, + .onscroll => w._on_scroll, + else => null, + }, else => return null, }; @@ -489,8 +533,6 @@ fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?j // DOM spec "retarget": walk original_target out of shadow trees until the // node is visible from current_target's tree. 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, @@ -512,8 +554,6 @@ fn getAdjustedTarget(original_target: ?*EventTarget, current_target: *EventTarge } fn isShadowIncludingInclusiveAncestor(ancestor: *Node, node: *Node) bool { - const ShadowRoot = @import("webapi/ShadowRoot.zig"); - var n: ?*Node = node; while (n) |cur| { if (cur == ancestor) { @@ -531,8 +571,6 @@ fn isShadowIncludingInclusiveAncestor(ancestor: *Node, node: *Node) bool { // Whether the target's tree root (without crossing shadow boundaries) is a // shadow root. Used for the spec's post-dispatch "clear targets" step. fn rootIsShadowRoot(target_: ?*EventTarget) bool { - const ShadowRoot = @import("webapi/ShadowRoot.zig"); - const target = target_ orelse return false; var current: *Node = switch (target._type) { .node => |n| n, diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index 00626ed12..5065b7141 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -237,8 +237,9 @@ fn AutoPrototypeChain(comptime types: []const type) type { fn eventInit(arena: Allocator, typ: String, value: anytype) !Event { // Round to 2ms for privacy (browsers do this) - const raw_timestamp = @import("../datetime.zig").milliTimestamp(.monotonic); - const time_stamp = (raw_timestamp / 2) * 2; + // Same (already coarsened) clock as the performance time origin, so the + // timeStamp getter can report it relative to that origin. + const time_stamp = @import("webapi/Performance.zig").highResTimestamp(); return .{ ._rc = .{}, diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index e78b1ea60..10548f3b8 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -21,6 +21,7 @@ const lp = @import("lightpanda"); const js = @import("../js/js.zig"); const Frame = @import("../Frame.zig"); +const Window = @import("Window.zig"); const URL = @import("../URL.zig"); const idna = @import("../../sys/idna.zig"); const public_suffix_list = @import("../../data/public_suffix_list.zig"); @@ -89,6 +90,22 @@ pub fn setOnSelectionChange(self: *Document, listener: ?js.Function) !void { } } +// Stored in the frame's attribute-listener map (like element and ShadowRoot +// property handlers), which the dispatch propagation path consults for any +// event target. +pub fn getOnClick(self: *Document, frame: *Frame) ?js.Function.Global { + return (self._frame orelse frame)._event_target_attr_listeners.get(.{ .target = self.asEventTarget(), .handler = .onclick }); +} + +pub fn setOnClick(self: *Document, setter: ?Window.FunctionSetter, frame: *Frame) !void { + const owner = self._frame orelse frame; + if (Window.getFunctionFromSetter(setter)) |cb| { + try owner._event_target_attr_listeners.put(owner.arena, .{ .target = self.asEventTarget(), .handler = .onclick }, cb); + } else { + _ = owner._event_target_attr_listeners.remove(.{ .target = self.asEventTarget(), .handler = .onclick }); + } +} + pub const Type = union(enum) { generic, html: *HTMLDocument, @@ -468,56 +485,85 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !* } const normalized = std.ascii.lowerString(&frame.buf, event_type); - if (std.mem.eql(u8, normalized, "event") or std.mem.eql(u8, normalized, "events") or std.mem.eql(u8, normalized, "htmlevents")) { - return Event.init("", null, frame._page); - } + const event: *Event = blk: { + if (std.mem.eql(u8, normalized, "event") or std.mem.eql(u8, normalized, "events") or std.mem.eql(u8, normalized, "htmlevents") or std.mem.eql(u8, normalized, "svgevents")) { + break :blk try Event.init("", null, frame._page); + } - if (std.mem.eql(u8, normalized, "customevent") or std.mem.eql(u8, normalized, "customevents")) { - const CustomEvent = @import("event/CustomEvent.zig"); - return (try CustomEvent.init("", null, frame._page)).asEvent(); - } + if (std.mem.eql(u8, normalized, "customevent") or std.mem.eql(u8, normalized, "customevents")) { + const CustomEvent = @import("event/CustomEvent.zig"); + break :blk (try CustomEvent.init("", null, frame._page)).asEvent(); + } - if (std.mem.eql(u8, normalized, "keyboardevent")) { - const KeyboardEvent = @import("event/KeyboardEvent.zig"); - return (try KeyboardEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "keyboardevent")) { + const KeyboardEvent = @import("event/KeyboardEvent.zig"); + break :blk (try KeyboardEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "inputevent")) { - const InputEvent = @import("event/InputEvent.zig"); - return (try InputEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "inputevent")) { + const InputEvent = @import("event/InputEvent.zig"); + break :blk (try InputEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) { - const MouseEvent = @import("event/MouseEvent.zig"); - return (try MouseEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "mouseevent") or std.mem.eql(u8, normalized, "mouseevents")) { + const MouseEvent = @import("event/MouseEvent.zig"); + break :blk (try MouseEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "messageevent")) { - const MessageEvent = @import("event/MessageEvent.zig"); - return (try MessageEvent.init("", null, frame._page)).asEvent(); - } + if (std.mem.eql(u8, normalized, "dragevent")) { + const DragEvent = @import("event/DragEvent.zig"); + break :blk (try DragEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "uievent") or std.mem.eql(u8, normalized, "uievents")) { - const UIEvent = @import("event/UIEvent.zig"); - return (try UIEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "messageevent")) { + const MessageEvent = @import("event/MessageEvent.zig"); + break :blk (try MessageEvent.init("", null, frame._page)).asEvent(); + } - if (std.mem.eql(u8, normalized, "focusevent") or std.mem.eql(u8, normalized, "focusevents")) { - const FocusEvent = @import("event/FocusEvent.zig"); - return (try FocusEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "hashchangeevent")) { + const HashChangeEvent = @import("event/HashChangeEvent.zig"); + break :blk (try HashChangeEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "textevent") or std.mem.eql(u8, normalized, "textevents")) { - const TextEvent = @import("event/TextEvent.zig"); - return (try TextEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "uievent") or std.mem.eql(u8, normalized, "uievents")) { + const UIEvent = @import("event/UIEvent.zig"); + break :blk (try UIEvent.init("", null, frame)).asEvent(); + } - if (std.mem.eql(u8, normalized, "compositionevent")) { - const CompositionEvent = @import("event/CompositionEvent.zig"); - return (try CompositionEvent.init("", null, frame)).asEvent(); - } + if (std.mem.eql(u8, normalized, "focusevent") or std.mem.eql(u8, normalized, "focusevents")) { + const FocusEvent = @import("event/FocusEvent.zig"); + break :blk (try FocusEvent.init("", null, frame)).asEvent(); + } - return error.NotSupported; + if (std.mem.eql(u8, normalized, "textevent") or std.mem.eql(u8, normalized, "textevents")) { + const TextEvent = @import("event/TextEvent.zig"); + break :blk (try TextEvent.init("", null, frame)).asEvent(); + } + + if (std.mem.eql(u8, normalized, "compositionevent")) { + const CompositionEvent = @import("event/CompositionEvent.zig"); + break :blk (try CompositionEvent.init("", null, frame)).asEvent(); + } + + // Aliases the spec requires createEvent to support but whose + // interfaces aren't implemented yet: return a plain Event so the + // caller can at least initialize and dispatch it. + if (std.mem.eql(u8, normalized, "beforeunloadevent") or + std.mem.eql(u8, normalized, "devicemotionevent") or + std.mem.eql(u8, normalized, "deviceorientationevent") or + std.mem.eql(u8, normalized, "storageevent")) + { + log.info(.not_implemented, "createEvent interface", .{ .type = event_type }); + break :blk try Event.init("", null, frame._page); + } + + return error.NotSupported; + }; + + // createEvent returns an uninitialized event: dispatching it before one + // of the init*Event calls throws an InvalidStateError. + event._initialized = false; + return event; } pub fn createTreeWalker(_: *const Document, root: *Node, what_to_show: ?js.Value, filter: ?DOMTreeWalker.FilterOpts, frame: *Frame) !*DOMTreeWalker { @@ -1265,6 +1311,7 @@ pub const JsApi = struct { } pub const onselectionchange = bridge.accessor(Document.getOnSelectionChange, Document.setOnSelectionChange, .{}); + pub const onclick = bridge.accessor(Document.getOnClick, Document.setOnClick, .{}); pub const URL = bridge.accessor(Document.getURL, null, .{}); pub const location = bridge.accessor(Document.getLocation, Document.setLocation, .{}); pub const documentURI = bridge.accessor(Document.getURL, null, .{}); diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 53179e125..298370fc7 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -50,6 +50,13 @@ _needs_retargeting: bool = false, _is_trusted: bool = false, _in_passive_listener: bool = false, _listeners_did_throw: bool = false, // IndexedDB needs to abort on callback throw +// Per spec, events created via document.createEvent are not initialized +// until one of the init*Event methods runs; dispatching one throws an +// InvalidStateError. Events created any other way start initialized. +_initialized: bool = true, +// Time origin of the event's relevant global, captured at creation when +// known; 0 means "use the accessing realm's origin" (see getTimeStamp). +_time_origin: u64 = 0, // There's a period of time between creating an event and handing it off to v8 // where things can fail. If it does fail, we need to deinit the event. The timing @@ -109,9 +116,9 @@ pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event { fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, comptime trusted: bool) !*Event { const opts = opts_ orelse Options{}; - // Round to 2ms for privacy (browsers do this) - const raw_timestamp = @import("../../datetime.zig").milliTimestamp(.monotonic); - const time_stamp = (raw_timestamp / 2) * 2; + // Same (already coarsened) clock as the performance time origin, so the + // timeStamp getter can report it relative to that origin. + const time_stamp = @import("Performance.zig").highResTimestamp(); const event = try arena.create(Event); event.* = .{ @@ -137,6 +144,7 @@ pub fn initEvent( return; } + self._initialized = true; self._type_string = try String.init(self._arena, event_string, .{}); self._bubbles = bubbles orelse false; self._cancelable = cancelable orelse false; @@ -257,8 +265,12 @@ pub fn getEventPhase(self: *const Event) u8 { return @intFromEnum(self._event_phase); } -pub fn getTimeStamp(self: *const Event) u64 { - return self._time_stamp; +pub fn getTimeStamp(self: *const Event, exec: *js.Execution) f64 { + const origin = if (self._time_origin != 0) self._time_origin else exec.performance()._time_origin; + if (self._time_stamp <= origin) { + return 0.0; + } + return @as(f64, @floatFromInt(self._time_stamp - origin)) / 1000.0; } pub fn setTrusted(self: *Event) void { @@ -470,7 +482,14 @@ pub const JsApi = struct { pub var class_id: bridge.ClassId = undefined; }; - pub const constructor = bridge.constructor(Event.init, .{}); + pub const constructor = bridge.constructor(struct { + fn wrap(typ: []const u8, opts_: ?Options, exec: *js.Execution) !*Event { + const event = try Event.init(typ, opts_, exec.page); + // capture the realm's time + event._time_origin = exec.performance()._time_origin; + return event; + } + }.wrap, .{}); pub const @"type" = bridge.accessor(Event.getType, null, .{}); pub const bubbles = bridge.accessor(Event.getBubbles, null, .{}); pub const cancelable = bridge.accessor(Event.getCancelable, null, .{}); diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 7fe689396..ce130a143 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -69,6 +69,10 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, exec: *js.Execution) !bo if (event._event_phase != .none) { return error.InvalidStateError; } + + if (!event._initialized) { + return error.InvalidStateError; + } event._is_trusted = false; switch (exec.js.global) { diff --git a/src/browser/webapi/Performance.zig b/src/browser/webapi/Performance.zig index f9cd4e77b..41edffd14 100644 --- a/src/browser/webapi/Performance.zig +++ b/src/browser/webapi/Performance.zig @@ -47,7 +47,7 @@ _delivery_scheduled: bool = false, /// Get high-resolution timestamp in microseconds, rounded to 5μs increments /// to match browser behavior (prevents fingerprinting) -fn highResTimestamp() u64 { +pub fn highResTimestamp() u64 { const ts = datetime.timespec(); const micros = @as(u64, @intCast(ts.sec)) * 1_000_000 + @as(u64, @intCast(@divTrunc(ts.nsec, 1_000))); // Round to nearest 5 microseconds (like Firefox default) diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index e374d0168..c42f1da6a 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -425,6 +425,21 @@ pub fn setOnScroll(self: *Window, setter: ?FunctionSetter) void { self._on_scroll = getFunctionFromSetter(setter); } +// Stored in the frame's attribute-listener map (like element and ShadowRoot +// property handlers), which the dispatch propagation path consults for any +// event target. +pub fn getOnClick(self: *Window) ?js.Function.Global { + return self._frame._event_target_attr_listeners.get(.{ .target = self.asEventTarget(), .handler = .onclick }); +} + +pub fn setOnClick(self: *Window, setter: ?FunctionSetter) !void { + if (getFunctionFromSetter(setter)) |cb| { + try self._frame._event_target_attr_listeners.put(self._frame.arena, .{ .target = self.asEventTarget(), .handler = .onclick }, cb); + } else { + _ = self._frame._event_target_attr_listeners.remove(.{ .target = self.asEventTarget(), .handler = .onclick }); + } +} + // The "window-reflecting body element event handler set" (HTML spec): these // event handlers of body and frameset elements are aliases for the Window's. // Returns the Window storage slot for the given content attribute name, or @@ -1159,6 +1174,7 @@ pub const JsApi = struct { pub const onfocus = bridge.accessor(Window.getOnFocus, Window.setOnFocus, .{}); pub const onresize = bridge.accessor(Window.getOnResize, Window.setOnResize, .{}); pub const onscroll = bridge.accessor(Window.getOnScroll, Window.setOnScroll, .{}); + pub const onclick = bridge.accessor(Window.getOnClick, Window.setOnClick, .{}); pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{}); pub const onrejectionhandled = bridge.accessor(Window.getOnRejectionHandled, Window.setOnRejectionHandled, .{}); pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{}); diff --git a/src/browser/webapi/event/CompositionEvent.zig b/src/browser/webapi/event/CompositionEvent.zig index fb3b4fd4d..93493d418 100644 --- a/src/browser/webapi/event/CompositionEvent.zig +++ b/src/browser/webapi/event/CompositionEvent.zig @@ -79,6 +79,7 @@ pub fn initCompositionEvent( } const arena = event._arena; + event._initialized = true; event._type_string = try String.init(arena, typ, .{}); event._bubbles = bubbles orelse false; event._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig index e1412a5d7..61f96e941 100644 --- a/src/browser/webapi/event/CustomEvent.zig +++ b/src/browser/webapi/event/CustomEvent.zig @@ -73,6 +73,7 @@ pub fn initCustomEvent( // This function can only be called after the constructor has called. // So we assume proto is initialized already by constructor. + self._proto._initialized = true; self._proto._type_string = try String.init(self._proto._arena, event_string, .{}); self._proto._bubbles = bubbles orelse false; self._proto._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/event/KeyboardEvent.zig b/src/browser/webapi/event/KeyboardEvent.zig index 73ff74706..5c66cde3d 100644 --- a/src/browser/webapi/event/KeyboardEvent.zig +++ b/src/browser/webapi/event/KeyboardEvent.zig @@ -428,6 +428,7 @@ pub fn initKeyboardEvent( } const arena = event._arena; + event._initialized = true; event._type_string = try String.init(arena, typ, .{}); event._bubbles = bubbles orelse false; event._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig index eac879547..819d6fe9c 100644 --- a/src/browser/webapi/event/MouseEvent.zig +++ b/src/browser/webapi/event/MouseEvent.zig @@ -239,6 +239,7 @@ pub fn initMouseEvent( return; } + event._initialized = true; event._type_string = try String.init(event._arena, typ, .{}); event._bubbles = bubbles orelse false; event._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/event/TextEvent.zig b/src/browser/webapi/event/TextEvent.zig index 8a5e238f8..f03cdd0dc 100644 --- a/src/browser/webapi/event/TextEvent.zig +++ b/src/browser/webapi/event/TextEvent.zig @@ -83,6 +83,7 @@ pub fn initTextEvent( } const arena = event._arena; + event._initialized = true; event._type_string = try String.init(arena, typ, .{}); event._bubbles = bubbles orelse false; event._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig index c5145e7f7..ded62c17a 100644 --- a/src/browser/webapi/event/UIEvent.zig +++ b/src/browser/webapi/event/UIEvent.zig @@ -146,6 +146,7 @@ pub fn initUIEvent( return; } + event._initialized = true; event._type_string = try String.init(event._arena, typ, .{}); event._bubbles = bubbles orelse false; event._cancelable = cancelable orelse false; diff --git a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig index cfa258fc0..275586e19 100644 --- a/src/browser/webapi/net/XMLHttpRequestEventTarget.zig +++ b/src/browser/webapi/net/XMLHttpRequestEventTarget.zig @@ -16,6 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . +const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); const EventTarget = @import("../EventTarget.zig"); @@ -83,6 +84,20 @@ pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchT ); } +// Resolves the property event handler for the given event type, so that a +// script-dispatched event (target.dispatchEvent) fires it like the internal +// dispatch path does. +pub fn inlineHandler(self: *const XMLHttpRequestEventTarget, typ: lp.String) ?js.Function.Global { + if (typ.eql(comptime .wrap("abort"))) return self._on_abort; + if (typ.eql(comptime .wrap("error"))) return self._on_error; + if (typ.eql(comptime .wrap("load"))) return self._on_load; + if (typ.eql(comptime .wrap("loadend"))) return self._on_load_end; + if (typ.eql(comptime .wrap("loadstart"))) return self._on_load_start; + if (typ.eql(comptime .wrap("progress"))) return self._on_progress; + if (typ.eql(comptime .wrap("timeout"))) return self._on_timeout; + return null; +} + pub fn getOnAbort(self: *const XMLHttpRequestEventTarget) ?js.Function.Global { return self._on_abort; }