mirror of
https://github.com/lightpanda-io/browser.git
synced 2026-07-30 17:25:58 -04:00
webapi: window-reflecting body/frameset event handlers
Fixes 24 failing tests in WPT /dom/events/Body-FrameSet-Event- Handlers.html (24/48 -> 48/48). Per the HTML spec, the onblur, onerror, onfocus, onload, onresize and onscroll event handlers of body and frameset elements are aliases for the Window's handlers (the "window-reflecting body element event handler set"), and setting the corresponding content attribute on those elements must activate the Window's handler too. Two groups of failures: - "Set ...": assigning a non-callable (e.g. "") to the IDL attribute threw "invalid argument" instead of storing null, because the setters took ?js.Function.Global. Per [LegacyTreatNonObjectAsNull], the Body/FrameSet setters now use Window.FunctionSetter (function or anything-else-means-null), like the existing Window handler setters. - "Forward ... to Window": only body.onload was forwarded. Window now stores onblur/onfocus/onresize/onscroll handlers too (and exposes the matching accessors, which it lacked entirely), and Body/FrameSet implement getters/setters forwarding all six handlers to the Window of the current frame. Setting or removing one of the six content attributes (at parse time via Build.complete or at runtime via the attributeChange/attributeRemove build hooks) compiles the handler body and stores it on the Window, so window.onX === element.onX. Coverage: /dom/events/Body-FrameSet-Event-Handlers.html 24/48 -> 48/48. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -82,6 +82,10 @@ _on_pageshow: ?js.Function.Global = null,
|
||||
_on_popstate: ?js.Function.Global = null,
|
||||
_on_hashchange: ?js.Function.Global = null,
|
||||
_on_error: ?js.Function.Global = null,
|
||||
_on_blur: ?js.Function.Global = null,
|
||||
_on_focus: ?js.Function.Global = null,
|
||||
_on_resize: ?js.Function.Global = null,
|
||||
_on_scroll: ?js.Function.Global = null,
|
||||
_on_message: ?js.Function.Global = null,
|
||||
_on_rejection_handled: ?js.Function.Global = null,
|
||||
_on_unhandled_rejection: ?js.Function.Global = null,
|
||||
@@ -388,6 +392,68 @@ pub fn setOnError(self: *Window, setter: ?FunctionSetter) void {
|
||||
self._on_error = getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnBlur(self: *const Window) ?js.Function.Global {
|
||||
return self._on_blur;
|
||||
}
|
||||
|
||||
pub fn setOnBlur(self: *Window, setter: ?FunctionSetter) void {
|
||||
self._on_blur = getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnFocus(self: *const Window) ?js.Function.Global {
|
||||
return self._on_focus;
|
||||
}
|
||||
|
||||
pub fn setOnFocus(self: *Window, setter: ?FunctionSetter) void {
|
||||
self._on_focus = getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnResize(self: *const Window) ?js.Function.Global {
|
||||
return self._on_resize;
|
||||
}
|
||||
|
||||
pub fn setOnResize(self: *Window, setter: ?FunctionSetter) void {
|
||||
self._on_resize = getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnScroll(self: *const Window) ?js.Function.Global {
|
||||
return self._on_scroll;
|
||||
}
|
||||
|
||||
pub fn setOnScroll(self: *Window, setter: ?FunctionSetter) void {
|
||||
self._on_scroll = getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
// The "window-reflecting body element event handler set" (HTML spec): these
|
||||
// event handlers of body and frameset elements are aliases for the Window's.
|
||||
// Returns the Window storage slot for the given content attribute name, or
|
||||
// null if the attribute isn't part of the set.
|
||||
pub fn windowReflectingHandler(self: *Window, name: lp.String) ?*?js.Function.Global {
|
||||
if (name.eql(comptime .wrap("onblur"))) return &self._on_blur;
|
||||
if (name.eql(comptime .wrap("onerror"))) return &self._on_error;
|
||||
if (name.eql(comptime .wrap("onfocus"))) return &self._on_focus;
|
||||
if (name.eql(comptime .wrap("onload"))) return &self._on_load;
|
||||
if (name.eql(comptime .wrap("onresize"))) return &self._on_resize;
|
||||
if (name.eql(comptime .wrap("onscroll"))) return &self._on_scroll;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Applies a window-reflecting content attribute (set on a body or frameset
|
||||
// element) to the Window's event handler. A null value clears the handler.
|
||||
pub fn setWindowReflectingHandlerFromAttribute(self: *Window, name: lp.String, value: ?[]const u8, frame: *Frame) void {
|
||||
const slot = self.windowReflectingHandler(name) orelse return;
|
||||
const expr = value orelse {
|
||||
slot.* = null;
|
||||
return;
|
||||
};
|
||||
if (frame.js.stringToPersistedFunction(expr, &.{"event"}, &.{})) |func| {
|
||||
slot.* = func;
|
||||
} else |err| {
|
||||
log.err(.js, "window reflecting handler", .{ .err = err, .str = expr });
|
||||
slot.* = null;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn getOnMessage(self: *const Window) ?js.Function.Global {
|
||||
return self._on_message;
|
||||
}
|
||||
@@ -1010,7 +1076,7 @@ const PostMessageCallback = struct {
|
||||
}
|
||||
};
|
||||
|
||||
const FunctionSetter = union(enum) {
|
||||
pub const FunctionSetter = union(enum) {
|
||||
func: js.Function.Global,
|
||||
anything: js.Value,
|
||||
};
|
||||
@@ -1018,7 +1084,7 @@ const FunctionSetter = union(enum) {
|
||||
// window.onload = {}; doesn't fail, but it doesn't do anything.
|
||||
// seems like setting to null is ok (though, at least on Firefix, it preserves
|
||||
// the original value, which we could do, but why?)
|
||||
fn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function.Global {
|
||||
pub fn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function.Global {
|
||||
const setter = setter_ orelse return null;
|
||||
return switch (setter) {
|
||||
.func => |func| func, // Already a Global from bridge auto-conversion
|
||||
@@ -1076,6 +1142,10 @@ pub const JsApi = struct {
|
||||
pub const onpopstate = bridge.accessor(Window.getOnPopState, Window.setOnPopState, .{});
|
||||
pub const onhashchange = bridge.accessor(Window.getOnHashChange, Window.setOnHashChange, .{});
|
||||
pub const onerror = bridge.accessor(Window.getOnError, Window.setOnError, .{});
|
||||
pub const onblur = bridge.accessor(Window.getOnBlur, Window.setOnBlur, .{});
|
||||
pub const onfocus = bridge.accessor(Window.getOnFocus, Window.setOnFocus, .{});
|
||||
pub const onresize = bridge.accessor(Window.getOnResize, Window.setOnResize, .{});
|
||||
pub const onscroll = bridge.accessor(Window.getOnScroll, Window.setOnScroll, .{});
|
||||
pub const onmessage = bridge.accessor(Window.getOnMessage, Window.setOnMessage, .{});
|
||||
pub const onrejectionhandled = bridge.accessor(Window.getOnRejectionHandled, Window.setOnRejectionHandled, .{});
|
||||
pub const onunhandledrejection = bridge.accessor(Window.getOnUnhandledRejection, Window.setOnUnhandledRejection, .{});
|
||||
|
||||
@@ -22,9 +22,10 @@ const Frame = @import("../../../Frame.zig");
|
||||
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const Window = @import("../../Window.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
|
||||
const log = lp.log;
|
||||
const String = lp.String;
|
||||
|
||||
const Body = @This();
|
||||
|
||||
@@ -37,15 +38,50 @@ pub fn asNode(self: *Body) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
/// Special-case: `body.onload` is actually an alias for `window.onload`.
|
||||
pub fn setOnLoad(_: *Body, callback: ?js.Function.Global, frame: *Frame) !void {
|
||||
frame.window._on_load = callback;
|
||||
// Special-case: the "window-reflecting body element event handler set"
|
||||
// (blur, error, focus, load, resize, scroll) are aliases for the Window's
|
||||
// event handlers.
|
||||
pub fn getOnBlur(_: *Body, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_blur;
|
||||
}
|
||||
pub fn setOnBlur(_: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_blur = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnError(_: *Body, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_error;
|
||||
}
|
||||
pub fn setOnError(_: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_error = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnFocus(_: *Body, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_focus;
|
||||
}
|
||||
pub fn setOnFocus(_: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_focus = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
/// Special-case: `body.onload` is actually an alias for `window.onload`.
|
||||
pub fn getOnLoad(_: *Body, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_load;
|
||||
}
|
||||
pub fn setOnLoad(_: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_load = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnResize(_: *Body, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_resize;
|
||||
}
|
||||
pub fn setOnResize(_: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_resize = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnScroll(_: *Body, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_scroll;
|
||||
}
|
||||
pub fn setOnScroll(_: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_scroll = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub const JsApi = struct {
|
||||
pub const bridge = js.Bridge(Body);
|
||||
@@ -56,17 +92,33 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const onblur = bridge.accessor(getOnBlur, setOnBlur, .{ .null_as_undefined = false });
|
||||
pub const onerror = bridge.accessor(getOnError, setOnError, .{ .null_as_undefined = false });
|
||||
pub const onfocus = bridge.accessor(getOnFocus, setOnFocus, .{ .null_as_undefined = false });
|
||||
pub const onload = bridge.accessor(getOnLoad, setOnLoad, .{ .null_as_undefined = false });
|
||||
pub const onresize = bridge.accessor(getOnResize, setOnResize, .{ .null_as_undefined = false });
|
||||
pub const onscroll = bridge.accessor(getOnScroll, setOnScroll, .{ .null_as_undefined = false });
|
||||
};
|
||||
|
||||
pub const Build = struct {
|
||||
const window_reflecting_attributes = [_][]const u8{
|
||||
"onblur", "onerror", "onfocus", "onload", "onresize", "onscroll",
|
||||
};
|
||||
|
||||
pub fn complete(node: *Node, frame: *Frame) !void {
|
||||
const el = node.as(Element);
|
||||
const on_load = el.getAttributeSafe(comptime .wrap("onload")) orelse return;
|
||||
if (frame.js.stringToPersistedFunction(on_load, &.{"event"}, &.{})) |func| {
|
||||
frame.window._on_load = func;
|
||||
} else |err| {
|
||||
log.err(.js, "body.onload", .{ .err = err, .str = on_load });
|
||||
inline for (window_reflecting_attributes) |attr| {
|
||||
if (el.getAttributeSafe(comptime .wrap(attr))) |value| {
|
||||
frame.window.setWindowReflectingHandlerFromAttribute(comptime .wrap(attr), value, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attributeChange(_: *Element, name: String, value: String, frame: *Frame) !void {
|
||||
frame.window.setWindowReflectingHandlerFromAttribute(name, value.str(), frame);
|
||||
}
|
||||
|
||||
pub fn attributeRemove(_: *Element, name: String, frame: *Frame) !void {
|
||||
frame.window.setWindowReflectingHandlerFromAttribute(name, null, frame);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1,9 +1,14 @@
|
||||
const lp = @import("lightpanda");
|
||||
|
||||
const js = @import("../../../js/js.zig");
|
||||
const Frame = @import("../../../Frame.zig");
|
||||
const Node = @import("../../Node.zig");
|
||||
const Element = @import("../../Element.zig");
|
||||
const Window = @import("../../Window.zig");
|
||||
const HtmlElement = @import("../Html.zig");
|
||||
|
||||
const String = lp.String;
|
||||
|
||||
const FrameSet = @This();
|
||||
|
||||
_proto: *HtmlElement,
|
||||
@@ -15,6 +20,51 @@ pub fn asNode(self: *FrameSet) *Node {
|
||||
return self.asElement().asNode();
|
||||
}
|
||||
|
||||
// Special-case: the "window-reflecting body element event handler set"
|
||||
// (blur, error, focus, load, resize, scroll) are aliases for the Window's
|
||||
// event handlers, on frameset elements just like on body elements.
|
||||
pub fn getOnBlur(_: *FrameSet, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_blur;
|
||||
}
|
||||
pub fn setOnBlur(_: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_blur = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnError(_: *FrameSet, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_error;
|
||||
}
|
||||
pub fn setOnError(_: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_error = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnFocus(_: *FrameSet, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_focus;
|
||||
}
|
||||
pub fn setOnFocus(_: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_focus = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnLoad(_: *FrameSet, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_load;
|
||||
}
|
||||
pub fn setOnLoad(_: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_load = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnResize(_: *FrameSet, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_resize;
|
||||
}
|
||||
pub fn setOnResize(_: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_resize = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getOnScroll(_: *FrameSet, frame: *Frame) ?js.Function.Global {
|
||||
return frame.window._on_scroll;
|
||||
}
|
||||
pub fn setOnScroll(_: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void {
|
||||
frame.window._on_scroll = Window.getFunctionFromSetter(setter);
|
||||
}
|
||||
|
||||
pub fn getCols(self: *FrameSet) []const u8 {
|
||||
return self.asElement().getAttributeSafe(comptime .wrap("cols")) orelse "";
|
||||
}
|
||||
@@ -42,6 +92,36 @@ pub const JsApi = struct {
|
||||
|
||||
pub const cols = bridge.accessor(FrameSet.getCols, FrameSet.setCols, .{ .ce_reactions = true });
|
||||
pub const rows = bridge.accessor(FrameSet.getRows, FrameSet.setRows, .{ .ce_reactions = true });
|
||||
|
||||
pub const onblur = bridge.accessor(getOnBlur, setOnBlur, .{ .null_as_undefined = false });
|
||||
pub const onerror = bridge.accessor(getOnError, setOnError, .{ .null_as_undefined = false });
|
||||
pub const onfocus = bridge.accessor(getOnFocus, setOnFocus, .{ .null_as_undefined = false });
|
||||
pub const onload = bridge.accessor(getOnLoad, setOnLoad, .{ .null_as_undefined = false });
|
||||
pub const onresize = bridge.accessor(getOnResize, setOnResize, .{ .null_as_undefined = false });
|
||||
pub const onscroll = bridge.accessor(getOnScroll, setOnScroll, .{ .null_as_undefined = false });
|
||||
};
|
||||
|
||||
pub const Build = struct {
|
||||
const window_reflecting_attributes = [_][]const u8{
|
||||
"onblur", "onerror", "onfocus", "onload", "onresize", "onscroll",
|
||||
};
|
||||
|
||||
pub fn complete(node: *Node, frame: *Frame) !void {
|
||||
const el = node.as(Element);
|
||||
inline for (window_reflecting_attributes) |attr| {
|
||||
if (el.getAttributeSafe(comptime .wrap(attr))) |value| {
|
||||
frame.window.setWindowReflectingHandlerFromAttribute(comptime .wrap(attr), value, frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn attributeChange(_: *Element, name: String, value: String, frame: *Frame) !void {
|
||||
frame.window.setWindowReflectingHandlerFromAttribute(name, value.str(), frame);
|
||||
}
|
||||
|
||||
pub fn attributeRemove(_: *Element, name: String, frame: *Frame) !void {
|
||||
frame.window.setWindowReflectingHandlerFromAttribute(name, null, frame);
|
||||
}
|
||||
};
|
||||
|
||||
const testing = @import("../../../../testing.zig");
|
||||
|
||||
Reference in New Issue
Block a user