From 89174ba0b60127092c48f4a9181449cd1a15fd26 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 27 Jan 2026 01:27:18 +0300 Subject: [PATCH 1/8] `EventManager`: introduce `inline_lookup` Idea with this is to have a key-to-function for known event listeners. We pack pointer to event target with listener type to generate key and set function as value. By doing this, we save bytes for optionally and rarely set functions in elements. --- src/browser/EventManager.zig | 150 +++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index fb6d3674..3e0d4d56 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -22,6 +22,7 @@ const builtin = @import("builtin"); const log = @import("../log.zig"); const String = @import("../string.zig").String; +const lp = @import("lightpanda"); const js = @import("js/js.zig"); const Page = @import("Page.zig"); @@ -42,11 +43,26 @@ list_pool: std.heap.MemoryPool(std.DoublyLinkedList), lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList), dispatch_depth: usize, deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }), +/// Use this when a listener provided like this: +/// +/// ```html +/// +/// ``` +/// +/// Or: +/// +/// ```js +/// img.onload = () => { ... }; +/// ``` +inline_lookup: std.AutoHashMapUnmanaged(usize, js.Function.Global), pub fn init(page: *Page) EventManager { + lp.assert(@alignOf(EventTarget) == 8, "EventManager.init", .{ .event_target_alignment = @alignOf(EventTarget) }); + return .{ .page = page, .lookup = .{}, + .inline_lookup = .{}, .arena = page.arena, .list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena), .listener_pool = std.heap.MemoryPool(Listener).init(page.arena), @@ -67,6 +83,32 @@ pub const Callback = union(enum) { object: js.Object, }; +/// Sets an inline event listener (`onload`, `onclick`, `onwheel` etc.); +/// overrides the listener if there's already one. +pub fn setInlineListener( + self: *EventManager, + event_target: *EventTarget, + event_type: Listener.Type, + listener_callback: js.Function.Global, +) !void { + if (comptime IS_DEBUG) { + log.debug(.event, "EventManager.setInlineListener", .{ .event_target = event_target, .event_type = event_type }); + } + + const key = createLookupKey(event_target, event_type); + const gop = try self.inline_lookup.getOrPut(self.arena, key); + gop.value_ptr.* = listener_callback; +} + +/// Returns the inline event listener by the `EventTarget` and event type. +pub fn getInlineListener( + self: *const EventManager, + event_target: *EventTarget, + event_type: Listener.Type, +) ?js.Function.Global { + return self.inline_lookup.get(createLookupKey(event_target, event_type)); +} + pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void { if (comptime IS_DEBUG) { log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target }); @@ -442,6 +484,16 @@ fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Ca return null; } +/// Creates a lookup key to use with `inline_lookup`. +inline fn createLookupKey(event_target: *EventTarget, event_type: Listener.Type) usize { + return @intFromPtr(event_target) >> 3 | (@as(u64, @intFromEnum(event_type)) << 57); +} + +/// Returns listener type from `inline_lookup` key. +inline fn getListenerType(key: usize) Listener.Type { + return @enumFromInt(key >> 57); +} + const Listener = struct { typ: String, once: bool, @@ -451,6 +503,104 @@ const Listener = struct { signal: ?*@import("webapi/AbortSignal.zig") = null, node: std.DoublyLinkedList.Node, removed: bool = false, + + const Type = enum(u7) { + abort, + animationcancel, + animationend, + animationiteration, + animationstart, + auxclick, + beforeinput, + beforematch, + beforetoggle, + blur, + cancel, + canplay, + canplaythrough, + change, + click, + close, + command, + contentvisibilityautostatechange, + contextlost, + contextmenu, + contextrestored, + copy, + cuechange, + cut, + dblclick, + drag, + dragend, + dragenter, + dragexit, + dragleave, + dragover, + dragstart, + drop, + durationchange, + emptied, + ended, + @"error", + focus, + formdata, + fullscreenchange, + fullscreenerror, + gotpointercapture, + input, + invalid, + keydown, + keypress, + keyup, + load, + loadeddata, + loadedmetadata, + loadstart, + lostpointercapture, + mousedown, + mousemove, + mouseout, + mouseover, + mouseup, + paste, + pause, + play, + playing, + pointercancel, + pointerdown, + pointerenter, + pointerleave, + pointermove, + pointerout, + pointerover, + pointerrawupdate, + pointerup, + progress, + ratechange, + reset, + resize, + scroll, + scrollend, + securitypolicyviolation, + seeked, + seeking, + select, + selectionchange, + selectstart, + slotchange, + stalled, + submit, + @"suspend", + timeupdate, + toggle, + transitioncancel, + transitionend, + transitionrun, + transitionstart, + volumechange, + waiting, + wheel, + }; }; const Function = union(enum) { From 6ad1a11593f442261fef8e52a410fa460e4eef64 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Tue, 27 Jan 2026 01:55:54 +0300 Subject: [PATCH 2/8] catch pointer overflows in `createLookupKey` Its better to have this; if this is incorrect, its better to get notified. --- src/browser/EventManager.zig | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 3e0d4d56..6ec65a9d 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -485,8 +485,10 @@ fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Ca } /// Creates a lookup key to use with `inline_lookup`. -inline fn createLookupKey(event_target: *EventTarget, event_type: Listener.Type) usize { - return @intFromPtr(event_target) >> 3 | (@as(u64, @intFromEnum(event_type)) << 57); +fn createLookupKey(event_target: *EventTarget, event_type: Listener.Type) usize { + const ptr = @intFromPtr(event_target) >> 3; + lp.assert(ptr < (1 << 57), "createLookupKey: pointer overflow", .{ .ptr = ptr }); + return ptr | (@as(u64, @intFromEnum(event_type)) << 57); } /// Returns listener type from `inline_lookup` key. From fd1e77df8facda519bbd23907b9d31a5f79ce7af Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 28 Jan 2026 01:31:43 +0300 Subject: [PATCH 3/8] parse event listeners provided as attributes --- src/browser/Page.zig | 232 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 830b29b1..ae580737 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -2173,6 +2173,238 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi } var attributes = try element.createAttributeList(self); while (list.next()) |attr| { + // Event handlers can be provided like attributes; here we check if there's such. + const name = attr.name.local; + lp.assert(name.len != 0, "populateElementAttributes: 0-length attr name", .{ .attr = attr }); + // Idea here is to make this check as cheap as possible. + const has_on_prefix = @as(u16, @bitCast([2]u8{ name.ptr[0], name.ptr[1 % name.len] })) == asUint("on"); + // We may have found an event handler. + if (has_on_prefix) { + // Must be usable as function. + const func = try self.js.stringToPersistedFunction(attr.value.slice()); + const target = element.asEventTarget(); + const event_manager = &self._event_manager; + + // Longest known listener kind is 32 bytes long. + const remaining: u6 = @truncate(name.len -| 2); + const unsafe = name.ptr + 2; + const Vec16x8 = @Vector(16, u8); + const Vec32x8 = @Vector(32, u8); + + switch (remaining) { + 3 => if (@as(u24, @bitCast(unsafe[0..3].*)) == asUint("cut")) { + try event_manager.setInlineListener(target, .cut, func); + }, + 4 => switch (@as(u32, @bitCast(unsafe[0..4].*))) { + asUint("blur") => try event_manager.setInlineListener(target, .blur, func), + asUint("copy") => try event_manager.setInlineListener(target, .copy, func), + asUint("drag") => try event_manager.setInlineListener(target, .drag, func), + asUint("drop") => try event_manager.setInlineListener(target, .drop, func), + asUint("load") => try event_manager.setInlineListener(target, .load, func), + asUint("play") => try event_manager.setInlineListener(target, .play, func), + else => {}, + }, + 5 => switch (@as(u40, @bitCast(unsafe[0..5].*))) { + asUint("abort") => try event_manager.setInlineListener(target, .abort, func), + asUint("click") => try event_manager.setInlineListener(target, .click, func), + asUint("close") => try event_manager.setInlineListener(target, .close, func), + asUint("ended") => try event_manager.setInlineListener(target, .ended, func), + asUint("error") => try event_manager.setInlineListener(target, .@"error", func), + asUint("focus") => try event_manager.setInlineListener(target, .focus, func), + asUint("input") => try event_manager.setInlineListener(target, .input, func), + asUint("keyup") => try event_manager.setInlineListener(target, .keyup, func), + asUint("paste") => try event_manager.setInlineListener(target, .paste, func), + asUint("pause") => try event_manager.setInlineListener(target, .pause, func), + asUint("reset") => try event_manager.setInlineListener(target, .reset, func), + asUint("wheel") => try event_manager.setInlineListener(target, .wheel, func), + else => {}, + }, + 6 => switch (@as(u48, @bitCast(unsafe[0..6].*))) { + asUint("cancel") => try event_manager.setInlineListener(target, .cancel, func), + asUint("change") => try event_manager.setInlineListener(target, .change, func), + asUint("resize") => try event_manager.setInlineListener(target, .resize, func), + asUint("scroll") => try event_manager.setInlineListener(target, .scroll, func), + asUint("seeked") => try event_manager.setInlineListener(target, .seeked, func), + asUint("select") => try event_manager.setInlineListener(target, .select, func), + asUint("submit") => try event_manager.setInlineListener(target, .submit, func), + asUint("toggle") => try event_manager.setInlineListener(target, .toggle, func), + else => {}, + }, + 7 => switch (@as(u56, @bitCast(unsafe[0..7].*))) { + asUint("canplay") => try event_manager.setInlineListener(target, .canplay, func), + asUint("command") => try event_manager.setInlineListener(target, .command, func), + asUint("dragend") => try event_manager.setInlineListener(target, .dragend, func), + asUint("emptied") => try event_manager.setInlineListener(target, .emptied, func), + asUint("invalid") => try event_manager.setInlineListener(target, .invalid, func), + asUint("keydown") => try event_manager.setInlineListener(target, .keydown, func), + asUint("mouseup") => try event_manager.setInlineListener(target, .mouseup, func), + asUint("playing") => try event_manager.setInlineListener(target, .playing, func), + asUint("seeking") => try event_manager.setInlineListener(target, .seeking, func), + asUint("stalled") => try event_manager.setInlineListener(target, .stalled, func), + asUint("suspend") => try event_manager.setInlineListener(target, .@"suspend", func), + asUint("waiting") => try event_manager.setInlineListener(target, .waiting, func), + else => {}, + }, + 8 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("auxclick") => try event_manager.setInlineListener(target, .auxclick, func), + asUint("dblclick") => try event_manager.setInlineListener(target, .dblclick, func), + asUint("dragexit") => try event_manager.setInlineListener(target, .dragexit, func), + asUint("dragover") => try event_manager.setInlineListener(target, .dragover, func), + asUint("formdata") => try event_manager.setInlineListener(target, .formdata, func), + asUint("keypress") => try event_manager.setInlineListener(target, .keypress, func), + asUint("mouseout") => try event_manager.setInlineListener(target, .mouseout, func), + asUint("progress") => try event_manager.setInlineListener(target, .progress, func), + else => {}, + }, + // Won't fit to 64-bit integer; we do 2 checks. + 9 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("cuechang") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .cuechange, func), + asUint("dragente") => if (unsafe[8] == 'r') try event_manager.setInlineListener(target, .dragenter, func), + asUint("dragleav") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .dragleave, func), + asUint("dragstar") => if (unsafe[8] == 't') try event_manager.setInlineListener(target, .dragstart, func), + asUint("loadstar") => if (unsafe[8] == 't') try event_manager.setInlineListener(target, .loadstart, func), + asUint("mousedow") => if (unsafe[8] == 'n') try event_manager.setInlineListener(target, .mousedown, func), + asUint("mousemov") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .mousemove, func), + asUint("mouseove") => if (unsafe[8] == 'r') try event_manager.setInlineListener(target, .mouseover, func), + asUint("pointeru") => if (unsafe[8] == 'p') try event_manager.setInlineListener(target, .pointerup, func), + asUint("scrollen") => if (unsafe[8] == 'd') try event_manager.setInlineListener(target, .scrollend, func), + else => {}, + }, + 10 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("loadedda") => if (asUint("ta") == @as(u16, @bitCast(unsafe[8..10].*))) + try event_manager.setInlineListener(target, .loadeddata, func), + asUint("pointero") => if (asUint("ut") == @as(u16, @bitCast(unsafe[8..10].*))) + try event_manager.setInlineListener(target, .pointerout, func), + asUint("ratechan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*))) + try event_manager.setInlineListener(target, .ratechange, func), + asUint("slotchan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*))) + try event_manager.setInlineListener(target, .slotchange, func), + asUint("timeupda") => if (asUint("te") == @as(u16, @bitCast(unsafe[8..10].*))) + try event_manager.setInlineListener(target, .timeupdate, func), + else => {}, + }, + 11 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("beforein") => if (asUint("put") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .beforeinput, func), + asUint("beforema") => if (asUint("tch") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .beforematch, func), + asUint("contextl") => if (asUint("ost") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .contextlost, func), + asUint("contextm") => if (asUint("enu") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .contextmenu, func), + asUint("pointerd") => if (asUint("own") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .pointerdown, func), + asUint("pointerm") => if (asUint("ove") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .pointermove, func), + asUint("pointero") => if (asUint("ver") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .pointerover, func), + asUint("selectst") => if (asUint("art") == @as(u24, @bitCast(unsafe[8..11].*))) + try event_manager.setInlineListener(target, .selectstart, func), + else => {}, + }, + 12 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("animatio") => if (asUint("nend") == @as(u32, @bitCast(unsafe[8..12].*))) + try event_manager.setInlineListener(target, .animationend, func), + asUint("beforeto") => if (asUint("ggle") == @as(u32, @bitCast(unsafe[8..12].*))) + try event_manager.setInlineListener(target, .beforetoggle, func), + asUint("pointere") => if (asUint("nter") == @as(u32, @bitCast(unsafe[8..12].*))) + try event_manager.setInlineListener(target, .pointerenter, func), + asUint("pointerl") => if (asUint("eave") == @as(u32, @bitCast(unsafe[8..12].*))) + try event_manager.setInlineListener(target, .pointerleave, func), + asUint("volumech") => if (asUint("ange") == @as(u32, @bitCast(unsafe[8..12].*))) + try event_manager.setInlineListener(target, .volumechange, func), + else => {}, + }, + 13 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("pointerc") => if (asUint("ancel") == @as(u40, @bitCast(unsafe[8..13].*))) + try event_manager.setInlineListener(target, .pointercancel, func), + asUint("transiti") => switch (@as(u40, @bitCast(unsafe[8..13].*))) { + asUint("onend") => try event_manager.setInlineListener(target, .transitionend, func), + asUint("onrun") => try event_manager.setInlineListener(target, .transitionrun, func), + else => {}, + }, + else => {}, + }, + 14 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("animatio") => if (asUint("nstart") == @as(u48, @bitCast(unsafe[8..14].*))) + try event_manager.setInlineListener(target, .animationstart, func), + asUint("canplayt") => if (asUint("hrough") == @as(u48, @bitCast(unsafe[8..14].*))) + try event_manager.setInlineListener(target, .canplaythrough, func), + asUint("duration") => if (asUint("change") == @as(u48, @bitCast(unsafe[8..14].*))) + try event_manager.setInlineListener(target, .durationchange, func), + asUint("loadedme") => if (asUint("tadata") == @as(u48, @bitCast(unsafe[8..14].*))) + try event_manager.setInlineListener(target, .loadedmetadata, func), + else => {}, + }, + 15 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { + asUint("animatio") => if (asUint("ncancel") == @as(u56, @bitCast(unsafe[8..15].*))) + try event_manager.setInlineListener(target, .animationcancel, func), + asUint("contextr") => if (asUint("estored") == @as(u56, @bitCast(unsafe[8..15].*))) + try event_manager.setInlineListener(target, .contextrestored, func), + asUint("fullscre") => if (asUint("enerror") == @as(u56, @bitCast(unsafe[8..15].*))) + try event_manager.setInlineListener(target, .fullscreenerror, func), + asUint("selectio") => if (asUint("nchange") == @as(u56, @bitCast(unsafe[8..15].*))) + try event_manager.setInlineListener(target, .selectionchange, func), + asUint("transiti") => if (asUint("onstart") == @as(u56, @bitCast(unsafe[8..15].*))) + try event_manager.setInlineListener(target, .transitionstart, func), + else => {}, + }, + // Can't switch on vector types. + 16 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + if (@reduce(.And, as_vector == @as(Vec16x8, "fullscreenchange".*))) { + try event_manager.setInlineListener(target, .fullscreenchange, func); + } else if (@reduce(.And, as_vector == @as(Vec16x8, "pointerrawupdate".*))) { + try event_manager.setInlineListener(target, .pointerrawupdate, func); + } else if (@reduce(.And, as_vector == @as(Vec16x8, "transitioncancel".*))) { + try event_manager.setInlineListener(target, .transitioncancel, func); + } + }, + 17 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + const dirty = @reduce(.And, as_vector == @as(Vec16x8, "gotpointercaptur".*)) and + unsafe[16] == 'e'; + if (dirty) { + try event_manager.setInlineListener(target, .gotpointercapture, func); + } + }, + 18 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + const is_animationiteration = @reduce(.And, as_vector == @as(Vec16x8, "animationiterati".*)) and + asUint("on") == @as(u16, @bitCast(unsafe[16..18].*)); + if (is_animationiteration) { + try event_manager.setInlineListener(target, .animationiteration, func); + } else { + const is_lostpointercapture = @reduce(.And, as_vector == @as(Vec16x8, "lostpointercaptu".*)) and + asUint("re") == @as(u16, @bitCast(unsafe[16..18].*)); + if (is_lostpointercapture) { + try event_manager.setInlineListener(target, .lostpointercapture, func); + } + } + }, + 23 => { + const as_vector: Vec16x8 = unsafe[0..16].*; + + const dirty = @reduce(.And, as_vector == @as(Vec16x8, "securitypolicyvi".*)) and + asUint("olation") == @as(u56, @bitCast(unsafe[16..23].*)); + if (dirty) { + try event_manager.setInlineListener(target, .securitypolicyviolation, func); + } + }, + 32 => { + const as_vector: Vec32x8 = unsafe[0..32].*; + + if (@reduce(.And, as_vector == @as(Vec32x8, "contentvisibilityautostatechange".*))) { + try event_manager.setInlineListener(target, .contentvisibilityautostatechange, func); + } + }, + else => {}, + } + } + try attributes.putNew(attr.name.local.slice(), attr.value.slice(), self); } } From 560f028bdac7a2236f0f186fd17dedbc7a21fa10 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 28 Jan 2026 01:33:17 +0300 Subject: [PATCH 4/8] remove unused `getListenerType` --- src/browser/EventManager.zig | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 6ec65a9d..90cc888c 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -491,11 +491,6 @@ fn createLookupKey(event_target: *EventTarget, event_type: Listener.Type) usize return ptr | (@as(u64, @intFromEnum(event_type)) << 57); } -/// Returns listener type from `inline_lookup` key. -inline fn getListenerType(key: usize) Listener.Type { - return @enumFromInt(key >> 57); -} - const Listener = struct { typ: String, once: bool, From 76a53bedbe095a3c20110fa0ab01476997871ad4 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 28 Jan 2026 17:26:56 +0300 Subject: [PATCH 5/8] split inline event listener logic to `Page.zig` and `Element.zig` --- src/browser/EventManager.zig | 146 -------------------- src/browser/Page.zig | 238 +++++++++++++++++++-------------- src/browser/webapi/Element.zig | 122 +++++++++++++++++ 3 files changed, 263 insertions(+), 243 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 90cc888c..c21792c4 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -43,26 +43,11 @@ list_pool: std.heap.MemoryPool(std.DoublyLinkedList), lookup: std.AutoHashMapUnmanaged(usize, *std.DoublyLinkedList), dispatch_depth: usize, deferred_removals: std.ArrayList(struct { list: *std.DoublyLinkedList, listener: *Listener }), -/// Use this when a listener provided like this: -/// -/// ```html -/// -/// ``` -/// -/// Or: -/// -/// ```js -/// img.onload = () => { ... }; -/// ``` -inline_lookup: std.AutoHashMapUnmanaged(usize, js.Function.Global), pub fn init(page: *Page) EventManager { - lp.assert(@alignOf(EventTarget) == 8, "EventManager.init", .{ .event_target_alignment = @alignOf(EventTarget) }); - return .{ .page = page, .lookup = .{}, - .inline_lookup = .{}, .arena = page.arena, .list_pool = std.heap.MemoryPool(std.DoublyLinkedList).init(page.arena), .listener_pool = std.heap.MemoryPool(Listener).init(page.arena), @@ -83,32 +68,6 @@ pub const Callback = union(enum) { object: js.Object, }; -/// Sets an inline event listener (`onload`, `onclick`, `onwheel` etc.); -/// overrides the listener if there's already one. -pub fn setInlineListener( - self: *EventManager, - event_target: *EventTarget, - event_type: Listener.Type, - listener_callback: js.Function.Global, -) !void { - if (comptime IS_DEBUG) { - log.debug(.event, "EventManager.setInlineListener", .{ .event_target = event_target, .event_type = event_type }); - } - - const key = createLookupKey(event_target, event_type); - const gop = try self.inline_lookup.getOrPut(self.arena, key); - gop.value_ptr.* = listener_callback; -} - -/// Returns the inline event listener by the `EventTarget` and event type. -pub fn getInlineListener( - self: *const EventManager, - event_target: *EventTarget, - event_type: Listener.Type, -) ?js.Function.Global { - return self.inline_lookup.get(createLookupKey(event_target, event_type)); -} - pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void { if (comptime IS_DEBUG) { log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once, .target = target }); @@ -484,13 +443,6 @@ fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Ca return null; } -/// Creates a lookup key to use with `inline_lookup`. -fn createLookupKey(event_target: *EventTarget, event_type: Listener.Type) usize { - const ptr = @intFromPtr(event_target) >> 3; - lp.assert(ptr < (1 << 57), "createLookupKey: pointer overflow", .{ .ptr = ptr }); - return ptr | (@as(u64, @intFromEnum(event_type)) << 57); -} - const Listener = struct { typ: String, once: bool, @@ -500,104 +452,6 @@ const Listener = struct { signal: ?*@import("webapi/AbortSignal.zig") = null, node: std.DoublyLinkedList.Node, removed: bool = false, - - const Type = enum(u7) { - abort, - animationcancel, - animationend, - animationiteration, - animationstart, - auxclick, - beforeinput, - beforematch, - beforetoggle, - blur, - cancel, - canplay, - canplaythrough, - change, - click, - close, - command, - contentvisibilityautostatechange, - contextlost, - contextmenu, - contextrestored, - copy, - cuechange, - cut, - dblclick, - drag, - dragend, - dragenter, - dragexit, - dragleave, - dragover, - dragstart, - drop, - durationchange, - emptied, - ended, - @"error", - focus, - formdata, - fullscreenchange, - fullscreenerror, - gotpointercapture, - input, - invalid, - keydown, - keypress, - keyup, - load, - loadeddata, - loadedmetadata, - loadstart, - lostpointercapture, - mousedown, - mousemove, - mouseout, - mouseover, - mouseup, - paste, - pause, - play, - playing, - pointercancel, - pointerdown, - pointerenter, - pointerleave, - pointermove, - pointerout, - pointerover, - pointerrawupdate, - pointerup, - progress, - ratechange, - reset, - resize, - scroll, - scrollend, - securitypolicyviolation, - seeked, - seeking, - select, - selectionchange, - selectstart, - slotchange, - stalled, - submit, - @"suspend", - timeupdate, - toggle, - transitioncancel, - transitionend, - transitionrun, - transitionstart, - volumechange, - waiting, - wheel, - }; }; const Function = union(enum) { diff --git a/src/browser/Page.zig b/src/browser/Page.zig index ae580737..5e4f1113 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -102,6 +102,20 @@ _element_shadow_roots: Element.ShadowRootLookup = .{}, _node_owner_documents: Node.OwnerDocumentLookup = .{}, _element_assigned_slots: Element.AssignedSlotLookup = .{}, +/// Lazily-created inline event listeners (or listeners provided as attributes). +/// Avoids bloating all elements with extra function fields for rare usage. +/// +/// Use this when a listener provided like these: +/// +/// ```html +/// +/// ``` +/// +/// ```js +/// img.onload = () => { ... }; +/// ``` +_element_attr_listeners: Element.AttrListenerLookup = .{}, + _script_manager: ScriptManager, // List of active MutationObservers @@ -316,6 +330,9 @@ fn reset(self: *Page, comptime initializing: bool) !void { self._element_shadow_roots = .{}; self._node_owner_documents = .{}; self._element_assigned_slots = .{}; + + self._element_attr_listeners = .{}; + self._notified_network_idle = .init; self._notified_network_almost_idle = .init; @@ -1134,6 +1151,35 @@ pub fn getElementByIdFromNode(self: *Page, node: *Node, id: []const u8) ?*Elemen return null; } +/// Sets an inline event listener (`onload`, `onclick`, `onwheel` etc.); +/// overrides the listener if there's already one. +pub fn setAttrListener( + self: *Page, + element: *Element, + listener_type: Element.KnownListener, + listener_callback: JS.Function.Global, +) !void { + if (comptime IS_DEBUG) { + log.debug(.event, "Page.setAttrListener", .{ + .element = element, + .listener_type = listener_type, + }); + } + + const key = element.calcAttrListenerKey(listener_type); + const gop = try self._element_attr_listeners.getOrPut(self.arena, key); + gop.value_ptr.* = listener_callback; +} + +/// Returns the inline event listener by an element and listener type. +pub fn getAttrListener( + self: *const Page, + element: *Element, + listener_type: Element.KnownListener, +) ?JS.Function.Global { + return self._element_attr_listeners.get(element.calcAttrListenerKey(listener_type)); +} + pub fn registerPerformanceObserver(self: *Page, observer: *PerformanceObserver) !void { return self._performance_observers.append(self.arena, observer); } @@ -2182,8 +2228,6 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi if (has_on_prefix) { // Must be usable as function. const func = try self.js.stringToPersistedFunction(attr.value.slice()); - const target = element.asEventTarget(); - const event_manager = &self._event_manager; // Longest known listener kind is 32 bytes long. const remaining: u6 = @truncate(name.len -| 2); @@ -2193,160 +2237,160 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi switch (remaining) { 3 => if (@as(u24, @bitCast(unsafe[0..3].*)) == asUint("cut")) { - try event_manager.setInlineListener(target, .cut, func); + try self.setAttrListener(element, .cut, func); }, 4 => switch (@as(u32, @bitCast(unsafe[0..4].*))) { - asUint("blur") => try event_manager.setInlineListener(target, .blur, func), - asUint("copy") => try event_manager.setInlineListener(target, .copy, func), - asUint("drag") => try event_manager.setInlineListener(target, .drag, func), - asUint("drop") => try event_manager.setInlineListener(target, .drop, func), - asUint("load") => try event_manager.setInlineListener(target, .load, func), - asUint("play") => try event_manager.setInlineListener(target, .play, func), + asUint("blur") => try self.setAttrListener(element, .blur, func), + asUint("copy") => try self.setAttrListener(element, .copy, func), + asUint("drag") => try self.setAttrListener(element, .drag, func), + asUint("drop") => try self.setAttrListener(element, .drop, func), + asUint("load") => try self.setAttrListener(element, .load, func), + asUint("play") => try self.setAttrListener(element, .play, func), else => {}, }, 5 => switch (@as(u40, @bitCast(unsafe[0..5].*))) { - asUint("abort") => try event_manager.setInlineListener(target, .abort, func), - asUint("click") => try event_manager.setInlineListener(target, .click, func), - asUint("close") => try event_manager.setInlineListener(target, .close, func), - asUint("ended") => try event_manager.setInlineListener(target, .ended, func), - asUint("error") => try event_manager.setInlineListener(target, .@"error", func), - asUint("focus") => try event_manager.setInlineListener(target, .focus, func), - asUint("input") => try event_manager.setInlineListener(target, .input, func), - asUint("keyup") => try event_manager.setInlineListener(target, .keyup, func), - asUint("paste") => try event_manager.setInlineListener(target, .paste, func), - asUint("pause") => try event_manager.setInlineListener(target, .pause, func), - asUint("reset") => try event_manager.setInlineListener(target, .reset, func), - asUint("wheel") => try event_manager.setInlineListener(target, .wheel, func), + asUint("abort") => try self.setAttrListener(element, .abort, func), + asUint("click") => try self.setAttrListener(element, .click, func), + asUint("close") => try self.setAttrListener(element, .close, func), + asUint("ended") => try self.setAttrListener(element, .ended, func), + asUint("error") => try self.setAttrListener(element, .@"error", func), + asUint("focus") => try self.setAttrListener(element, .focus, func), + asUint("input") => try self.setAttrListener(element, .input, func), + asUint("keyup") => try self.setAttrListener(element, .keyup, func), + asUint("paste") => try self.setAttrListener(element, .paste, func), + asUint("pause") => try self.setAttrListener(element, .pause, func), + asUint("reset") => try self.setAttrListener(element, .reset, func), + asUint("wheel") => try self.setAttrListener(element, .wheel, func), else => {}, }, 6 => switch (@as(u48, @bitCast(unsafe[0..6].*))) { - asUint("cancel") => try event_manager.setInlineListener(target, .cancel, func), - asUint("change") => try event_manager.setInlineListener(target, .change, func), - asUint("resize") => try event_manager.setInlineListener(target, .resize, func), - asUint("scroll") => try event_manager.setInlineListener(target, .scroll, func), - asUint("seeked") => try event_manager.setInlineListener(target, .seeked, func), - asUint("select") => try event_manager.setInlineListener(target, .select, func), - asUint("submit") => try event_manager.setInlineListener(target, .submit, func), - asUint("toggle") => try event_manager.setInlineListener(target, .toggle, func), + asUint("cancel") => try self.setAttrListener(element, .cancel, func), + asUint("change") => try self.setAttrListener(element, .change, func), + asUint("resize") => try self.setAttrListener(element, .resize, func), + asUint("scroll") => try self.setAttrListener(element, .scroll, func), + asUint("seeked") => try self.setAttrListener(element, .seeked, func), + asUint("select") => try self.setAttrListener(element, .select, func), + asUint("submit") => try self.setAttrListener(element, .submit, func), + asUint("toggle") => try self.setAttrListener(element, .toggle, func), else => {}, }, 7 => switch (@as(u56, @bitCast(unsafe[0..7].*))) { - asUint("canplay") => try event_manager.setInlineListener(target, .canplay, func), - asUint("command") => try event_manager.setInlineListener(target, .command, func), - asUint("dragend") => try event_manager.setInlineListener(target, .dragend, func), - asUint("emptied") => try event_manager.setInlineListener(target, .emptied, func), - asUint("invalid") => try event_manager.setInlineListener(target, .invalid, func), - asUint("keydown") => try event_manager.setInlineListener(target, .keydown, func), - asUint("mouseup") => try event_manager.setInlineListener(target, .mouseup, func), - asUint("playing") => try event_manager.setInlineListener(target, .playing, func), - asUint("seeking") => try event_manager.setInlineListener(target, .seeking, func), - asUint("stalled") => try event_manager.setInlineListener(target, .stalled, func), - asUint("suspend") => try event_manager.setInlineListener(target, .@"suspend", func), - asUint("waiting") => try event_manager.setInlineListener(target, .waiting, func), + asUint("canplay") => try self.setAttrListener(element, .canplay, func), + asUint("command") => try self.setAttrListener(element, .command, func), + asUint("dragend") => try self.setAttrListener(element, .dragend, func), + asUint("emptied") => try self.setAttrListener(element, .emptied, func), + asUint("invalid") => try self.setAttrListener(element, .invalid, func), + asUint("keydown") => try self.setAttrListener(element, .keydown, func), + asUint("mouseup") => try self.setAttrListener(element, .mouseup, func), + asUint("playing") => try self.setAttrListener(element, .playing, func), + asUint("seeking") => try self.setAttrListener(element, .seeking, func), + asUint("stalled") => try self.setAttrListener(element, .stalled, func), + asUint("suspend") => try self.setAttrListener(element, .@"suspend", func), + asUint("waiting") => try self.setAttrListener(element, .waiting, func), else => {}, }, 8 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { - asUint("auxclick") => try event_manager.setInlineListener(target, .auxclick, func), - asUint("dblclick") => try event_manager.setInlineListener(target, .dblclick, func), - asUint("dragexit") => try event_manager.setInlineListener(target, .dragexit, func), - asUint("dragover") => try event_manager.setInlineListener(target, .dragover, func), - asUint("formdata") => try event_manager.setInlineListener(target, .formdata, func), - asUint("keypress") => try event_manager.setInlineListener(target, .keypress, func), - asUint("mouseout") => try event_manager.setInlineListener(target, .mouseout, func), - asUint("progress") => try event_manager.setInlineListener(target, .progress, func), + asUint("auxclick") => try self.setAttrListener(element, .auxclick, func), + asUint("dblclick") => try self.setAttrListener(element, .dblclick, func), + asUint("dragexit") => try self.setAttrListener(element, .dragexit, func), + asUint("dragover") => try self.setAttrListener(element, .dragover, func), + asUint("formdata") => try self.setAttrListener(element, .formdata, func), + asUint("keypress") => try self.setAttrListener(element, .keypress, func), + asUint("mouseout") => try self.setAttrListener(element, .mouseout, func), + asUint("progress") => try self.setAttrListener(element, .progress, func), else => {}, }, // Won't fit to 64-bit integer; we do 2 checks. 9 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { - asUint("cuechang") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .cuechange, func), - asUint("dragente") => if (unsafe[8] == 'r') try event_manager.setInlineListener(target, .dragenter, func), - asUint("dragleav") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .dragleave, func), - asUint("dragstar") => if (unsafe[8] == 't') try event_manager.setInlineListener(target, .dragstart, func), - asUint("loadstar") => if (unsafe[8] == 't') try event_manager.setInlineListener(target, .loadstart, func), - asUint("mousedow") => if (unsafe[8] == 'n') try event_manager.setInlineListener(target, .mousedown, func), - asUint("mousemov") => if (unsafe[8] == 'e') try event_manager.setInlineListener(target, .mousemove, func), - asUint("mouseove") => if (unsafe[8] == 'r') try event_manager.setInlineListener(target, .mouseover, func), - asUint("pointeru") => if (unsafe[8] == 'p') try event_manager.setInlineListener(target, .pointerup, func), - asUint("scrollen") => if (unsafe[8] == 'd') try event_manager.setInlineListener(target, .scrollend, func), + asUint("cuechang") => if (unsafe[8] == 'e') try self.setAttrListener(element, .cuechange, func), + asUint("dragente") => if (unsafe[8] == 'r') try self.setAttrListener(element, .dragenter, func), + asUint("dragleav") => if (unsafe[8] == 'e') try self.setAttrListener(element, .dragleave, func), + asUint("dragstar") => if (unsafe[8] == 't') try self.setAttrListener(element, .dragstart, func), + asUint("loadstar") => if (unsafe[8] == 't') try self.setAttrListener(element, .loadstart, func), + asUint("mousedow") => if (unsafe[8] == 'n') try self.setAttrListener(element, .mousedown, func), + asUint("mousemov") => if (unsafe[8] == 'e') try self.setAttrListener(element, .mousemove, func), + asUint("mouseove") => if (unsafe[8] == 'r') try self.setAttrListener(element, .mouseover, func), + asUint("pointeru") => if (unsafe[8] == 'p') try self.setAttrListener(element, .pointerup, func), + asUint("scrollen") => if (unsafe[8] == 'd') try self.setAttrListener(element, .scrollend, func), else => {}, }, 10 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("loadedda") => if (asUint("ta") == @as(u16, @bitCast(unsafe[8..10].*))) - try event_manager.setInlineListener(target, .loadeddata, func), + try self.setAttrListener(element, .loadeddata, func), asUint("pointero") => if (asUint("ut") == @as(u16, @bitCast(unsafe[8..10].*))) - try event_manager.setInlineListener(target, .pointerout, func), + try self.setAttrListener(element, .pointerout, func), asUint("ratechan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*))) - try event_manager.setInlineListener(target, .ratechange, func), + try self.setAttrListener(element, .ratechange, func), asUint("slotchan") => if (asUint("ge") == @as(u16, @bitCast(unsafe[8..10].*))) - try event_manager.setInlineListener(target, .slotchange, func), + try self.setAttrListener(element, .slotchange, func), asUint("timeupda") => if (asUint("te") == @as(u16, @bitCast(unsafe[8..10].*))) - try event_manager.setInlineListener(target, .timeupdate, func), + try self.setAttrListener(element, .timeupdate, func), else => {}, }, 11 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("beforein") => if (asUint("put") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .beforeinput, func), + try self.setAttrListener(element, .beforeinput, func), asUint("beforema") => if (asUint("tch") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .beforematch, func), + try self.setAttrListener(element, .beforematch, func), asUint("contextl") => if (asUint("ost") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .contextlost, func), + try self.setAttrListener(element, .contextlost, func), asUint("contextm") => if (asUint("enu") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .contextmenu, func), + try self.setAttrListener(element, .contextmenu, func), asUint("pointerd") => if (asUint("own") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .pointerdown, func), + try self.setAttrListener(element, .pointerdown, func), asUint("pointerm") => if (asUint("ove") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .pointermove, func), + try self.setAttrListener(element, .pointermove, func), asUint("pointero") => if (asUint("ver") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .pointerover, func), + try self.setAttrListener(element, .pointerover, func), asUint("selectst") => if (asUint("art") == @as(u24, @bitCast(unsafe[8..11].*))) - try event_manager.setInlineListener(target, .selectstart, func), + try self.setAttrListener(element, .selectstart, func), else => {}, }, 12 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("animatio") => if (asUint("nend") == @as(u32, @bitCast(unsafe[8..12].*))) - try event_manager.setInlineListener(target, .animationend, func), + try self.setAttrListener(element, .animationend, func), asUint("beforeto") => if (asUint("ggle") == @as(u32, @bitCast(unsafe[8..12].*))) - try event_manager.setInlineListener(target, .beforetoggle, func), + try self.setAttrListener(element, .beforetoggle, func), asUint("pointere") => if (asUint("nter") == @as(u32, @bitCast(unsafe[8..12].*))) - try event_manager.setInlineListener(target, .pointerenter, func), + try self.setAttrListener(element, .pointerenter, func), asUint("pointerl") => if (asUint("eave") == @as(u32, @bitCast(unsafe[8..12].*))) - try event_manager.setInlineListener(target, .pointerleave, func), + try self.setAttrListener(element, .pointerleave, func), asUint("volumech") => if (asUint("ange") == @as(u32, @bitCast(unsafe[8..12].*))) - try event_manager.setInlineListener(target, .volumechange, func), + try self.setAttrListener(element, .volumechange, func), else => {}, }, 13 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("pointerc") => if (asUint("ancel") == @as(u40, @bitCast(unsafe[8..13].*))) - try event_manager.setInlineListener(target, .pointercancel, func), + try self.setAttrListener(element, .pointercancel, func), asUint("transiti") => switch (@as(u40, @bitCast(unsafe[8..13].*))) { - asUint("onend") => try event_manager.setInlineListener(target, .transitionend, func), - asUint("onrun") => try event_manager.setInlineListener(target, .transitionrun, func), + asUint("onend") => try self.setAttrListener(element, .transitionend, func), + asUint("onrun") => try self.setAttrListener(element, .transitionrun, func), else => {}, }, else => {}, }, 14 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("animatio") => if (asUint("nstart") == @as(u48, @bitCast(unsafe[8..14].*))) - try event_manager.setInlineListener(target, .animationstart, func), + try self.setAttrListener(element, .animationstart, func), asUint("canplayt") => if (asUint("hrough") == @as(u48, @bitCast(unsafe[8..14].*))) - try event_manager.setInlineListener(target, .canplaythrough, func), + try self.setAttrListener(element, .canplaythrough, func), asUint("duration") => if (asUint("change") == @as(u48, @bitCast(unsafe[8..14].*))) - try event_manager.setInlineListener(target, .durationchange, func), + try self.setAttrListener(element, .durationchange, func), asUint("loadedme") => if (asUint("tadata") == @as(u48, @bitCast(unsafe[8..14].*))) - try event_manager.setInlineListener(target, .loadedmetadata, func), + try self.setAttrListener(element, .loadedmetadata, func), else => {}, }, 15 => switch (@as(u64, @bitCast(unsafe[0..8].*))) { asUint("animatio") => if (asUint("ncancel") == @as(u56, @bitCast(unsafe[8..15].*))) - try event_manager.setInlineListener(target, .animationcancel, func), + try self.setAttrListener(element, .animationcancel, func), asUint("contextr") => if (asUint("estored") == @as(u56, @bitCast(unsafe[8..15].*))) - try event_manager.setInlineListener(target, .contextrestored, func), + try self.setAttrListener(element, .contextrestored, func), asUint("fullscre") => if (asUint("enerror") == @as(u56, @bitCast(unsafe[8..15].*))) - try event_manager.setInlineListener(target, .fullscreenerror, func), + try self.setAttrListener(element, .fullscreenerror, func), asUint("selectio") => if (asUint("nchange") == @as(u56, @bitCast(unsafe[8..15].*))) - try event_manager.setInlineListener(target, .selectionchange, func), + try self.setAttrListener(element, .selectionchange, func), asUint("transiti") => if (asUint("onstart") == @as(u56, @bitCast(unsafe[8..15].*))) - try event_manager.setInlineListener(target, .transitionstart, func), + try self.setAttrListener(element, .transitionstart, func), else => {}, }, // Can't switch on vector types. @@ -2354,11 +2398,11 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi const as_vector: Vec16x8 = unsafe[0..16].*; if (@reduce(.And, as_vector == @as(Vec16x8, "fullscreenchange".*))) { - try event_manager.setInlineListener(target, .fullscreenchange, func); + try self.setAttrListener(element, .fullscreenchange, func); } else if (@reduce(.And, as_vector == @as(Vec16x8, "pointerrawupdate".*))) { - try event_manager.setInlineListener(target, .pointerrawupdate, func); + try self.setAttrListener(element, .pointerrawupdate, func); } else if (@reduce(.And, as_vector == @as(Vec16x8, "transitioncancel".*))) { - try event_manager.setInlineListener(target, .transitioncancel, func); + try self.setAttrListener(element, .transitioncancel, func); } }, 17 => { @@ -2367,7 +2411,7 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi const dirty = @reduce(.And, as_vector == @as(Vec16x8, "gotpointercaptur".*)) and unsafe[16] == 'e'; if (dirty) { - try event_manager.setInlineListener(target, .gotpointercapture, func); + try self.setAttrListener(element, .gotpointercapture, func); } }, 18 => { @@ -2376,12 +2420,12 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi const is_animationiteration = @reduce(.And, as_vector == @as(Vec16x8, "animationiterati".*)) and asUint("on") == @as(u16, @bitCast(unsafe[16..18].*)); if (is_animationiteration) { - try event_manager.setInlineListener(target, .animationiteration, func); + try self.setAttrListener(element, .animationiteration, func); } else { const is_lostpointercapture = @reduce(.And, as_vector == @as(Vec16x8, "lostpointercaptu".*)) and asUint("re") == @as(u16, @bitCast(unsafe[16..18].*)); if (is_lostpointercapture) { - try event_manager.setInlineListener(target, .lostpointercapture, func); + try self.setAttrListener(element, .lostpointercapture, func); } } }, @@ -2391,14 +2435,14 @@ fn populateElementAttributes(self: *Page, element: *Element, list: anytype) !voi const dirty = @reduce(.And, as_vector == @as(Vec16x8, "securitypolicyvi".*)) and asUint("olation") == @as(u56, @bitCast(unsafe[16..23].*)); if (dirty) { - try event_manager.setInlineListener(target, .securitypolicyviolation, func); + try self.setAttrListener(element, .securitypolicyviolation, func); } }, 32 => { const as_vector: Vec32x8 = unsafe[0..32].*; if (@reduce(.And, as_vector == @as(Vec32x8, "contentvisibilityautostatechange".*))) { - try event_manager.setInlineListener(target, .contentvisibilityautostatechange, func); + try self.setAttrListener(element, .contentvisibilityautostatechange, func); } }, else => {}, diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index f983b514..512f5821 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -48,6 +48,128 @@ pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMT pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot); pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot); +/// Better to discriminate it since not directly a pointer int. +/// +/// See `calcAttrListenerKey` to obtain one. +pub const AttrListenerKey = u64; +/// Use `getAttrListenerKey` to create a key. +pub const AttrListenerLookup = std.AutoHashMapUnmanaged(AttrListenerKey, js.Function.Global); + +/// Enum of known event listeners; increasing the size of it (u7) +/// can cause `AttrListenerKey` to behave incorrectly. +pub const KnownListener = enum(u7) { + abort, + animationcancel, + animationend, + animationiteration, + animationstart, + auxclick, + beforeinput, + beforematch, + beforetoggle, + blur, + cancel, + canplay, + canplaythrough, + change, + click, + close, + command, + contentvisibilityautostatechange, + contextlost, + contextmenu, + contextrestored, + copy, + cuechange, + cut, + dblclick, + drag, + dragend, + dragenter, + dragexit, + dragleave, + dragover, + dragstart, + drop, + durationchange, + emptied, + ended, + @"error", + focus, + formdata, + fullscreenchange, + fullscreenerror, + gotpointercapture, + input, + invalid, + keydown, + keypress, + keyup, + load, + loadeddata, + loadedmetadata, + loadstart, + lostpointercapture, + mousedown, + mousemove, + mouseout, + mouseover, + mouseup, + paste, + pause, + play, + playing, + pointercancel, + pointerdown, + pointerenter, + pointerleave, + pointermove, + pointerout, + pointerover, + pointerrawupdate, + pointerup, + progress, + ratechange, + reset, + resize, + scroll, + scrollend, + securitypolicyviolation, + seeked, + seeking, + select, + selectionchange, + selectstart, + slotchange, + stalled, + submit, + @"suspend", + timeupdate, + toggle, + transitioncancel, + transitionend, + transitionrun, + transitionstart, + volumechange, + waiting, + wheel, +}; + +/// Calculates a lookup key (`AttrListenerKey`) to use with `AttrListenerLookup` for an element. +/// NEVER use generated key to retrieve a pointer back. Portability is not guaranteed. +pub fn calcAttrListenerKey(self: *Element, event_type: KnownListener) AttrListenerKey { + // We can use `Element` for the key too; `EventTarget` is strict about + // its size and alignment, though. + const target = self.asEventTarget(); + // Check if we have 3 bits available from alignment of 8. + lp.assert(@alignOf(@TypeOf(target)) == 8, "createLookupKey: incorrect alignment", .{ + .event_target_alignment = @alignOf(@TypeOf(target)), + }); + + const ptr = @intFromPtr(target) >> 3; + lp.assert(ptr < (1 << 57), "createLookupKey: pointer overflow", .{ .ptr = ptr }); + return ptr | (@as(u64, @intFromEnum(event_type)) << 57); +} pub const Namespace = enum(u8) { html, From 9f5c2e4ca761c34ca4be3b4ab42f8d43ca367033 Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 28 Jan 2026 17:27:22 +0300 Subject: [PATCH 6/8] add getter/setter functions for attribute event listeners Spec say these belong to `HTMLElement`. --- src/browser/webapi/element/Html.zig | 856 ++++++++++++++++++++++++++++ 1 file changed, 856 insertions(+) diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index 9eb41edd..d7831227 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -330,6 +330,766 @@ pub fn click(self: *HtmlElement, page: *Page) !void { try page._event_manager.dispatch(self.asEventTarget(), event.asEvent()); } +pub fn setOnAbort(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .abort, callback); +} + +pub fn getOnAbort(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .abort); +} + +pub fn setOnAnimationCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationcancel, callback); +} + +pub fn getOnAnimationCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationcancel); +} + +pub fn setOnAnimationEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationend, callback); +} + +pub fn getOnAnimationEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationend); +} + +pub fn setOnAnimationIteration(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationiteration, callback); +} + +pub fn getOnAnimationIteration(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationiteration); +} + +pub fn setOnAnimationStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .animationstart, callback); +} + +pub fn getOnAnimationStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .animationstart); +} + +pub fn setOnAuxClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .auxclick, callback); +} + +pub fn getOnAuxClick(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .auxclick); +} + +pub fn setOnBeforeInput(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .beforeinput, callback); +} + +pub fn getOnBeforeInput(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .beforeinput); +} + +pub fn setOnBeforeMatch(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .beforematch, callback); +} + +pub fn getOnBeforeMatch(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .beforematch); +} + +pub fn setOnBeforeToggle(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .beforetoggle, callback); +} + +pub fn getOnBeforeToggle(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .beforetoggle); +} + +pub fn setOnBlur(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .blur, callback); +} + +pub fn getOnBlur(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .blur); +} + +pub fn setOnCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .cancel, callback); +} + +pub fn getOnCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .cancel); +} + +pub fn setOnCanPlay(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .canplay, callback); +} + +pub fn getOnCanPlay(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .canplay); +} + +pub fn setOnCanPlayThrough(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .canplaythrough, callback); +} + +pub fn getOnCanPlayThrough(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .canplaythrough); +} + +pub fn setOnChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .change, callback); +} + +pub fn getOnChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .change); +} + +pub fn setOnClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .click, callback); +} + +pub fn getOnClick(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .click); +} + +pub fn setOnClose(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .close, callback); +} + +pub fn getOnClose(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .close); +} + +pub fn setOnCommand(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .command, callback); +} + +pub fn getOnCommand(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .command); +} + +pub fn setOnContentVisibilityAutoStateChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contentvisibilityautostatechange, callback); +} + +pub fn getOnContentVisibilityAutoStateChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contentvisibilityautostatechange); +} + +pub fn setOnContextLost(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contextlost, callback); +} + +pub fn getOnContextLost(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contextlost); +} + +pub fn setOnContextMenu(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contextmenu, callback); +} + +pub fn getOnContextMenu(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contextmenu); +} + +pub fn setOnContextRestored(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .contextrestored, callback); +} + +pub fn getOnContextRestored(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .contextrestored); +} + +pub fn setOnCopy(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .copy, callback); +} + +pub fn getOnCopy(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .copy); +} + +pub fn setOnCueChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .cuechange, callback); +} + +pub fn getOnCueChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .cuechange); +} + +pub fn setOnCut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .cut, callback); +} + +pub fn getOnCut(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .cut); +} + +pub fn setOnDblClick(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dblclick, callback); +} + +pub fn getOnDblClick(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dblclick); +} + +pub fn setOnDrag(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .drag, callback); +} + +pub fn getOnDrag(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .drag); +} + +pub fn setOnDragEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragend, callback); +} + +pub fn getOnDragEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragend); +} + +pub fn setOnDragEnter(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragenter, callback); +} + +pub fn getOnDragEnter(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragenter); +} + +pub fn setOnDragExit(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragexit, callback); +} + +pub fn getOnDragExit(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragexit); +} + +pub fn setOnDragLeave(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragleave, callback); +} + +pub fn getOnDragLeave(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragleave); +} + +pub fn setOnDragOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragover, callback); +} + +pub fn getOnDragOver(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragover); +} + +pub fn setOnDragStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .dragstart, callback); +} + +pub fn getOnDragStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .dragstart); +} + +pub fn setOnDrop(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .drop, callback); +} + +pub fn getOnDrop(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .drop); +} + +pub fn setOnDurationChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .durationchange, callback); +} + +pub fn getOnDurationChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .durationchange); +} + +pub fn setOnEmptied(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .emptied, callback); +} + +pub fn getOnEmptied(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .emptied); +} + +pub fn setOnEnded(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .ended, callback); +} + +pub fn getOnEnded(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .ended); +} + +pub fn setOnError(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .@"error", callback); +} + +pub fn getOnError(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .@"error"); +} + +pub fn setOnFocus(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .focus, callback); +} + +pub fn getOnFocus(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .focus); +} + +pub fn setOnFormData(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .formdata, callback); +} + +pub fn getOnFormData(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .formdata); +} + +pub fn setOnFullscreenChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .fullscreenchange, callback); +} + +pub fn getOnFullscreenChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .fullscreenchange); +} + +pub fn setOnFullscreenError(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .fullscreenerror, callback); +} + +pub fn getOnFullscreenError(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .fullscreenerror); +} + +pub fn setOnGotPointerCapture(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .gotpointercapture, callback); +} + +pub fn getOnGotPointerCapture(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .gotpointercapture); +} + +pub fn setOnInput(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .input, callback); +} + +pub fn getOnInput(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .input); +} + +pub fn setOnInvalid(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .invalid, callback); +} + +pub fn getOnInvalid(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .invalid); +} + +pub fn setOnKeyDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .keydown, callback); +} + +pub fn getOnKeyDown(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .keydown); +} + +pub fn setOnKeyPress(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .keypress, callback); +} + +pub fn getOnKeyPress(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .keypress); +} + +pub fn setOnKeyUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .keyup, callback); +} + +pub fn getOnKeyUp(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .keyup); +} + +pub fn setOnLoad(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .load, callback); +} + +pub fn getOnLoad(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .load); +} + +pub fn setOnLoadedData(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .loadeddata, callback); +} + +pub fn getOnLoadedData(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .loadeddata); +} + +pub fn setOnLoadedMetadata(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .loadedmetadata, callback); +} + +pub fn getOnLoadedMetadata(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .loadedmetadata); +} + +pub fn setOnLoadStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .loadstart, callback); +} + +pub fn getOnLoadStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .loadstart); +} + +pub fn setOnLostPointerCapture(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .lostpointercapture, callback); +} + +pub fn getOnLostPointerCapture(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .lostpointercapture); +} + +pub fn setOnMouseDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mousedown, callback); +} + +pub fn getOnMouseDown(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mousedown); +} + +pub fn setOnMouseMove(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mousemove, callback); +} + +pub fn getOnMouseMove(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mousemove); +} + +pub fn setOnMouseOut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mouseout, callback); +} + +pub fn getOnMouseOut(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mouseout); +} + +pub fn setOnMouseOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mouseover, callback); +} + +pub fn getOnMouseOver(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mouseover); +} + +pub fn setOnMouseUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .mouseup, callback); +} + +pub fn getOnMouseUp(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .mouseup); +} + +pub fn setOnPaste(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .paste, callback); +} + +pub fn getOnPaste(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .paste); +} + +pub fn setOnPause(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pause, callback); +} + +pub fn getOnPause(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pause); +} + +pub fn setOnPlay(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .play, callback); +} + +pub fn getOnPlay(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .play); +} + +pub fn setOnPlaying(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .playing, callback); +} + +pub fn getOnPlaying(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .playing); +} + +pub fn setOnPointerCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointercancel, callback); +} + +pub fn getOnPointerCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointercancel); +} + +pub fn setOnPointerDown(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerdown, callback); +} + +pub fn getOnPointerDown(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerdown); +} + +pub fn setOnPointerEnter(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerenter, callback); +} + +pub fn getOnPointerEnter(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerenter); +} + +pub fn setOnPointerLeave(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerleave, callback); +} + +pub fn getOnPointerLeave(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerleave); +} + +pub fn setOnPointerMove(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointermove, callback); +} + +pub fn getOnPointerMove(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointermove); +} + +pub fn setOnPointerOut(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerout, callback); +} + +pub fn getOnPointerOut(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerout); +} + +pub fn setOnPointerOver(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerover, callback); +} + +pub fn getOnPointerOver(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerover); +} + +pub fn setOnPointerRawUpdate(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerrawupdate, callback); +} + +pub fn getOnPointerRawUpdate(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerrawupdate); +} + +pub fn setOnPointerUp(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .pointerup, callback); +} + +pub fn getOnPointerUp(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .pointerup); +} + +pub fn setOnProgress(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .progress, callback); +} + +pub fn getOnProgress(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .progress); +} + +pub fn setOnRateChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .ratechange, callback); +} + +pub fn getOnRateChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .ratechange); +} + +pub fn setOnReset(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .reset, callback); +} + +pub fn getOnReset(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .reset); +} + +pub fn setOnResize(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .resize, callback); +} + +pub fn getOnResize(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .resize); +} + +pub fn setOnScroll(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .scroll, callback); +} + +pub fn getOnScroll(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .scroll); +} + +pub fn setOnScrollEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .scrollend, callback); +} + +pub fn getOnScrollEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .scrollend); +} + +pub fn setOnSecurityPolicyViolation(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .securitypolicyviolation, callback); +} + +pub fn getOnSecurityPolicyViolation(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .securitypolicyviolation); +} + +pub fn setOnSeeked(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .seeked, callback); +} + +pub fn getOnSeeked(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .seeked); +} + +pub fn setOnSeeking(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .seeking, callback); +} + +pub fn getOnSeeking(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .seeking); +} + +pub fn setOnSelect(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .select, callback); +} + +pub fn getOnSelect(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .select); +} + +pub fn setOnSelectionChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .selectionchange, callback); +} + +pub fn getOnSelectionChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .selectionchange); +} + +pub fn setOnSelectStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .selectstart, callback); +} + +pub fn getOnSelectStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .selectstart); +} + +pub fn setOnSlotChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .slotchange, callback); +} + +pub fn getOnSlotChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .slotchange); +} + +pub fn setOnStalled(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .stalled, callback); +} + +pub fn getOnStalled(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .stalled); +} + +pub fn setOnSubmit(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .submit, callback); +} + +pub fn getOnSubmit(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .submit); +} + +pub fn setOnSuspend(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .@"suspend", callback); +} + +pub fn getOnSuspend(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .@"suspend"); +} + +pub fn setOnTimeUpdate(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .timeupdate, callback); +} + +pub fn getOnTimeUpdate(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .timeupdate); +} + +pub fn setOnToggle(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .toggle, callback); +} + +pub fn getOnToggle(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .toggle); +} + +pub fn setOnTransitionCancel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitioncancel, callback); +} + +pub fn getOnTransitionCancel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitioncancel); +} + +pub fn setOnTransitionEnd(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitionend, callback); +} + +pub fn getOnTransitionEnd(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitionend); +} + +pub fn setOnTransitionRun(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitionrun, callback); +} + +pub fn getOnTransitionRun(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitionrun); +} + +pub fn setOnTransitionStart(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .transitionstart, callback); +} + +pub fn getOnTransitionStart(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .transitionstart); +} + +pub fn setOnVolumeChange(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .volumechange, callback); +} + +pub fn getOnVolumeChange(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .volumechange); +} + +pub fn setOnWaiting(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .waiting, callback); +} + +pub fn getOnWaiting(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .waiting); +} + +pub fn setOnWheel(self: *HtmlElement, callback: js.Function.Global, page: *Page) !void { + return page.setAttrListener(self.asElement(), .wheel, callback); +} + +pub fn getOnWheel(self: *HtmlElement, page: *Page) ?js.Function.Global { + return page.getAttrListener(self.asElement(), .wheel); +} + pub const JsApi = struct { pub const bridge = js.Bridge(HtmlElement); @@ -349,6 +1109,102 @@ pub const JsApi = struct { } pub const insertAdjacentHTML = bridge.function(HtmlElement.insertAdjacentHTML, .{ .dom_exception = true }); pub const click = bridge.function(HtmlElement.click, .{}); + + pub const onabort = bridge.accessor(HtmlElement.getOnAbort, HtmlElement.setOnAbort, .{}); + pub const onanimationcancel = bridge.accessor(HtmlElement.getOnAnimationCancel, HtmlElement.setOnAnimationCancel, .{}); + pub const onanimationend = bridge.accessor(HtmlElement.getOnAnimationEnd, HtmlElement.setOnAnimationEnd, .{}); + pub const onanimationiteration = bridge.accessor(HtmlElement.getOnAnimationIteration, HtmlElement.setOnAnimationIteration, .{}); + pub const onanimationstart = bridge.accessor(HtmlElement.getOnAnimationStart, HtmlElement.setOnAnimationStart, .{}); + pub const onauxclick = bridge.accessor(HtmlElement.getOnAuxClick, HtmlElement.setOnAuxClick, .{}); + pub const onbeforeinput = bridge.accessor(HtmlElement.getOnBeforeInput, HtmlElement.setOnBeforeInput, .{}); + pub const onbeforematch = bridge.accessor(HtmlElement.getOnBeforeMatch, HtmlElement.setOnBeforeMatch, .{}); + pub const onbeforetoggle = bridge.accessor(HtmlElement.getOnBeforeToggle, HtmlElement.setOnBeforeToggle, .{}); + pub const onblur = bridge.accessor(HtmlElement.getOnBlur, HtmlElement.setOnBlur, .{}); + pub const oncancel = bridge.accessor(HtmlElement.getOnCancel, HtmlElement.setOnCancel, .{}); + pub const oncanplay = bridge.accessor(HtmlElement.getOnCanPlay, HtmlElement.setOnCanPlay, .{}); + pub const oncanplaythrough = bridge.accessor(HtmlElement.getOnCanPlayThrough, HtmlElement.setOnCanPlayThrough, .{}); + pub const onchange = bridge.accessor(HtmlElement.getOnChange, HtmlElement.setOnChange, .{}); + pub const onclick = bridge.accessor(HtmlElement.getOnClick, HtmlElement.setOnClick, .{}); + pub const onclose = bridge.accessor(HtmlElement.getOnClose, HtmlElement.setOnClose, .{}); + pub const oncommand = bridge.accessor(HtmlElement.getOnCommand, HtmlElement.setOnCommand, .{}); + pub const oncontentvisibilityautostatechange = bridge.accessor(HtmlElement.getOnContentVisibilityAutoStateChange, HtmlElement.setOnContentVisibilityAutoStateChange, .{}); + pub const oncontextlost = bridge.accessor(HtmlElement.getOnContextLost, HtmlElement.setOnContextLost, .{}); + pub const oncontextmenu = bridge.accessor(HtmlElement.getOnContextMenu, HtmlElement.setOnContextMenu, .{}); + pub const oncontextrestored = bridge.accessor(HtmlElement.getOnContextRestored, HtmlElement.setOnContextRestored, .{}); + pub const oncopy = bridge.accessor(HtmlElement.getOnCopy, HtmlElement.setOnCopy, .{}); + pub const oncuechange = bridge.accessor(HtmlElement.getOnCueChange, HtmlElement.setOnCueChange, .{}); + pub const oncut = bridge.accessor(HtmlElement.getOnCut, HtmlElement.setOnCut, .{}); + pub const ondblclick = bridge.accessor(HtmlElement.getOnDblClick, HtmlElement.setOnDblClick, .{}); + pub const ondrag = bridge.accessor(HtmlElement.getOnDrag, HtmlElement.setOnDrag, .{}); + pub const ondragend = bridge.accessor(HtmlElement.getOnDragEnd, HtmlElement.setOnDragEnd, .{}); + pub const ondragenter = bridge.accessor(HtmlElement.getOnDragEnter, HtmlElement.setOnDragEnter, .{}); + pub const ondragexit = bridge.accessor(HtmlElement.getOnDragExit, HtmlElement.setOnDragExit, .{}); + pub const ondragleave = bridge.accessor(HtmlElement.getOnDragLeave, HtmlElement.setOnDragLeave, .{}); + pub const ondragover = bridge.accessor(HtmlElement.getOnDragOver, HtmlElement.setOnDragOver, .{}); + pub const ondragstart = bridge.accessor(HtmlElement.getOnDragStart, HtmlElement.setOnDragStart, .{}); + pub const ondrop = bridge.accessor(HtmlElement.getOnDrop, HtmlElement.setOnDrop, .{}); + pub const ondurationchange = bridge.accessor(HtmlElement.getOnDurationChange, HtmlElement.setOnDurationChange, .{}); + pub const onemptied = bridge.accessor(HtmlElement.getOnEmptied, HtmlElement.setOnEmptied, .{}); + pub const onended = bridge.accessor(HtmlElement.getOnEnded, HtmlElement.setOnEnded, .{}); + pub const onerror = bridge.accessor(HtmlElement.getOnError, HtmlElement.setOnError, .{}); + pub const onfocus = bridge.accessor(HtmlElement.getOnFocus, HtmlElement.setOnFocus, .{}); + pub const onformdata = bridge.accessor(HtmlElement.getOnFormData, HtmlElement.setOnFormData, .{}); + pub const onfullscreenchange = bridge.accessor(HtmlElement.getOnFullscreenChange, HtmlElement.setOnFullscreenChange, .{}); + pub const onfullscreenerror = bridge.accessor(HtmlElement.getOnFullscreenError, HtmlElement.setOnFullscreenError, .{}); + pub const ongotpointercapture = bridge.accessor(HtmlElement.getOnGotPointerCapture, HtmlElement.setOnGotPointerCapture, .{}); + pub const oninput = bridge.accessor(HtmlElement.getOnInput, HtmlElement.setOnInput, .{}); + pub const oninvalid = bridge.accessor(HtmlElement.getOnInvalid, HtmlElement.setOnInvalid, .{}); + pub const onkeydown = bridge.accessor(HtmlElement.getOnKeyDown, HtmlElement.setOnKeyDown, .{}); + pub const onkeypress = bridge.accessor(HtmlElement.getOnKeyPress, HtmlElement.setOnKeyPress, .{}); + pub const onkeyup = bridge.accessor(HtmlElement.getOnKeyUp, HtmlElement.setOnKeyUp, .{}); + pub const onload = bridge.accessor(HtmlElement.getOnLoad, HtmlElement.setOnLoad, .{}); + pub const onloadeddata = bridge.accessor(HtmlElement.getOnLoadedData, HtmlElement.setOnLoadedData, .{}); + pub const onloadedmetadata = bridge.accessor(HtmlElement.getOnLoadedMetadata, HtmlElement.setOnLoadedMetadata, .{}); + pub const onloadstart = bridge.accessor(HtmlElement.getOnLoadStart, HtmlElement.setOnLoadStart, .{}); + pub const onlostpointercapture = bridge.accessor(HtmlElement.getOnLostPointerCapture, HtmlElement.setOnLostPointerCapture, .{}); + pub const onmousedown = bridge.accessor(HtmlElement.getOnMouseDown, HtmlElement.setOnMouseDown, .{}); + pub const onmousemove = bridge.accessor(HtmlElement.getOnMouseMove, HtmlElement.setOnMouseMove, .{}); + pub const onmouseout = bridge.accessor(HtmlElement.getOnMouseOut, HtmlElement.setOnMouseOut, .{}); + pub const onmouseover = bridge.accessor(HtmlElement.getOnMouseOver, HtmlElement.setOnMouseOver, .{}); + pub const onmouseup = bridge.accessor(HtmlElement.getOnMouseUp, HtmlElement.setOnMouseUp, .{}); + pub const onpaste = bridge.accessor(HtmlElement.getOnPaste, HtmlElement.setOnPaste, .{}); + pub const onpause = bridge.accessor(HtmlElement.getOnPause, HtmlElement.setOnPause, .{}); + pub const onplay = bridge.accessor(HtmlElement.getOnPlay, HtmlElement.setOnPlay, .{}); + pub const onplaying = bridge.accessor(HtmlElement.getOnPlaying, HtmlElement.setOnPlaying, .{}); + pub const onpointercancel = bridge.accessor(HtmlElement.getOnPointerCancel, HtmlElement.setOnPointerCancel, .{}); + pub const onpointerdown = bridge.accessor(HtmlElement.getOnPointerDown, HtmlElement.setOnPointerDown, .{}); + pub const onpointerenter = bridge.accessor(HtmlElement.getOnPointerEnter, HtmlElement.setOnPointerEnter, .{}); + pub const onpointerleave = bridge.accessor(HtmlElement.getOnPointerLeave, HtmlElement.setOnPointerLeave, .{}); + pub const onpointermove = bridge.accessor(HtmlElement.getOnPointerMove, HtmlElement.setOnPointerMove, .{}); + pub const onpointerout = bridge.accessor(HtmlElement.getOnPointerOut, HtmlElement.setOnPointerOut, .{}); + pub const onpointerover = bridge.accessor(HtmlElement.getOnPointerOver, HtmlElement.setOnPointerOver, .{}); + pub const onpointerrawupdate = bridge.accessor(HtmlElement.getOnPointerRawUpdate, HtmlElement.setOnPointerRawUpdate, .{}); + pub const onpointerup = bridge.accessor(HtmlElement.getOnPointerUp, HtmlElement.setOnPointerUp, .{}); + pub const onprogress = bridge.accessor(HtmlElement.getOnProgress, HtmlElement.setOnProgress, .{}); + pub const onratechange = bridge.accessor(HtmlElement.getOnRateChange, HtmlElement.setOnRateChange, .{}); + pub const onreset = bridge.accessor(HtmlElement.getOnReset, HtmlElement.setOnReset, .{}); + pub const onresize = bridge.accessor(HtmlElement.getOnResize, HtmlElement.setOnResize, .{}); + pub const onscroll = bridge.accessor(HtmlElement.getOnScroll, HtmlElement.setOnScroll, .{}); + pub const onscrollend = bridge.accessor(HtmlElement.getOnScrollEnd, HtmlElement.setOnScrollEnd, .{}); + pub const onsecuritypolicyviolation = bridge.accessor(HtmlElement.getOnSecurityPolicyViolation, HtmlElement.setOnSecurityPolicyViolation, .{}); + pub const onseeked = bridge.accessor(HtmlElement.getOnSeeked, HtmlElement.setOnSeeked, .{}); + pub const onseeking = bridge.accessor(HtmlElement.getOnSeeking, HtmlElement.setOnSeeking, .{}); + pub const onselect = bridge.accessor(HtmlElement.getOnSelect, HtmlElement.setOnSelect, .{}); + pub const onselectionchange = bridge.accessor(HtmlElement.getOnSelectionChange, HtmlElement.setOnSelectionChange, .{}); + pub const onselectstart = bridge.accessor(HtmlElement.getOnSelectStart, HtmlElement.setOnSelectStart, .{}); + pub const onslotchange = bridge.accessor(HtmlElement.getOnSlotChange, HtmlElement.setOnSlotChange, .{}); + pub const onstalled = bridge.accessor(HtmlElement.getOnStalled, HtmlElement.setOnStalled, .{}); + pub const onsubmit = bridge.accessor(HtmlElement.getOnSubmit, HtmlElement.setOnSubmit, .{}); + pub const onsuspend = bridge.accessor(HtmlElement.getOnSuspend, HtmlElement.setOnSuspend, .{}); + pub const ontimeupdate = bridge.accessor(HtmlElement.getOnTimeUpdate, HtmlElement.setOnTimeUpdate, .{}); + pub const ontoggle = bridge.accessor(HtmlElement.getOnToggle, HtmlElement.setOnToggle, .{}); + pub const ontransitioncancel = bridge.accessor(HtmlElement.getOnTransitionCancel, HtmlElement.setOnTransitionCancel, .{}); + pub const ontransitionend = bridge.accessor(HtmlElement.getOnTransitionEnd, HtmlElement.setOnTransitionEnd, .{}); + pub const ontransitionrun = bridge.accessor(HtmlElement.getOnTransitionRun, HtmlElement.setOnTransitionRun, .{}); + pub const ontransitionstart = bridge.accessor(HtmlElement.getOnTransitionStart, HtmlElement.setOnTransitionStart, .{}); + pub const onvolumechange = bridge.accessor(HtmlElement.getOnVolumeChange, HtmlElement.setOnVolumeChange, .{}); + pub const onwaiting = bridge.accessor(HtmlElement.getOnWaiting, HtmlElement.setOnWaiting, .{}); + pub const onwheel = bridge.accessor(HtmlElement.getOnWheel, HtmlElement.setOnWheel, .{}); }; pub const Build = struct { From 0a68be695d0280d6e797d33c25c53aa5e2ada0bd Mon Sep 17 00:00:00 2001 From: Halil Durak Date: Wed, 28 Jan 2026 17:46:27 +0300 Subject: [PATCH 7/8] add tests --- .../tests/element/html/event_listeners.html | 490 ++++++++++++++++++ src/browser/webapi/element/Html.zig | 5 + 2 files changed, 495 insertions(+) create mode 100644 src/browser/tests/element/html/event_listeners.html diff --git a/src/browser/tests/element/html/event_listeners.html b/src/browser/tests/element/html/event_listeners.html new file mode 100644 index 00000000..9e7f9ebd --- /dev/null +++ b/src/browser/tests/element/html/event_listeners.html @@ -0,0 +1,490 @@ + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/browser/webapi/element/Html.zig b/src/browser/webapi/element/Html.zig index d7831227..244e7bed 100644 --- a/src/browser/webapi/element/Html.zig +++ b/src/browser/webapi/element/Html.zig @@ -1235,3 +1235,8 @@ pub const Build = struct { return false; } }; + +const testing = @import("../../../testing.zig"); +test "WebApi: HTML.event_listeners" { + try testing.htmlRunner("element/html/event_listeners.html", .{}); +} From dfe5c2440483d0a4429e03c754f29e4d412e8ccf Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 29 Jan 2026 07:04:20 +0800 Subject: [PATCH 8/8] remove unused import and unused export --- src/browser/EventManager.zig | 1 - src/browser/webapi/Element.zig | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index c21792c4..fb6d3674 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -22,7 +22,6 @@ const builtin = @import("builtin"); const log = @import("../log.zig"); const String = @import("../string.zig").String; -const lp = @import("lightpanda"); const js = @import("js/js.zig"); const Page = @import("Page.zig"); diff --git a/src/browser/webapi/Element.zig b/src/browser/webapi/Element.zig index 512f5821..619d95cd 100644 --- a/src/browser/webapi/Element.zig +++ b/src/browser/webapi/Element.zig @@ -48,10 +48,11 @@ pub const ClassListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMT pub const RelListLookup = std.AutoHashMapUnmanaged(*Element, *collections.DOMTokenList); pub const ShadowRootLookup = std.AutoHashMapUnmanaged(*Element, *ShadowRoot); pub const AssignedSlotLookup = std.AutoHashMapUnmanaged(*Element, *Html.Slot); + /// Better to discriminate it since not directly a pointer int. /// /// See `calcAttrListenerKey` to obtain one. -pub const AttrListenerKey = u64; +const AttrListenerKey = u64; /// Use `getAttrListenerKey` to create a key. pub const AttrListenerLookup = std.AutoHashMapUnmanaged(AttrListenerKey, js.Function.Global);