diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index 3caae9054..416d70cc0 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -515,6 +515,10 @@ fn handleError(comptime T: type, comptime F: type, local: *const Local, err: any const js_err: *const v8.Value = switch (err) { error.TryCatchRethrow => return, + // A JS exception is already pending in the isolate (e.g. a value's + // toString threw during argument conversion); throwing anything here + // would replace the original exception the script expects to see. + error.JsException => return, error.InvalidArgument => isolate.createTypeError("invalid argument"), error.TypeError => isolate.createTypeError(""), error.RangeError => isolate.createRangeError(""), @@ -1016,6 +1020,11 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info: // type instantiation of jsValueToZig may not include such errors // in its inferred error set. @field(args, tupleFieldName(field_index)) = local.jsValueToZig(param.type.?, js_val) catch |err| { + if (err == error.JsException) { + // an exception thrown by user code (e.g. a toString + // getter) is pending; propagate it untouched + return err; + } const DOMException = @import("../webapi/DOMException.zig"); if (DOMException.fromError(err) != null) { return err; diff --git a/src/browser/tests/frames/support/window_reflecting.html b/src/browser/tests/frames/support/window_reflecting.html new file mode 100644 index 000000000..b84a50668 --- /dev/null +++ b/src/browser/tests/frames/support/window_reflecting.html @@ -0,0 +1,6 @@ + + + +

x

+ diff --git a/src/browser/tests/frames/window_reflecting_handlers.html b/src/browser/tests/frames/window_reflecting_handlers.html new file mode 100644 index 000000000..0150f13d1 --- /dev/null +++ b/src/browser/tests/frames/window_reflecting_handlers.html @@ -0,0 +1,53 @@ + + + + + + + + + + + diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 2265822a2..7fe689396 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -23,6 +23,7 @@ const Page = @import("../Page.zig"); const EventManager = @import("../EventManager.zig"); const Event = @import("Event.zig"); +const AbortSignal = @import("AbortSignal.zig"); const RegisterOptions = EventManager.RegisterOptions; @@ -83,7 +84,17 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, exec: *js.Execution) !bo const AddEventListenerOptions = union(enum) { capture: bool, - options: RegisterOptions, + options: Options, + + // The signal is kept as a raw js.Value so that an explicit null (or any + // non-AbortSignal value) can be told apart from an absent or undefined + // member, and rejected with a TypeError per the dictionary conversion. + const Options = struct { + once: bool = false, + capture: bool = false, + passive: bool = false, + signal: ?js.Value = null, + }; }; pub const EventListenerCallback = union(enum) { @@ -91,6 +102,27 @@ pub const EventListenerCallback = union(enum) { object: js.Object, }; pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?AddEventListenerOptions, exec: *js.Execution) !void { + // Convert the options before the null-callback early return: per spec, + // the dictionary conversion throws even when the callback is null. + const options = blk: { + const o = opts_ orelse break :blk RegisterOptions{}; + break :blk switch (o) { + .options => |opts| RegisterOptions{ + .once = opts.once, + .capture = opts.capture, + .passive = opts.passive, + .signal = signal: { + const signal = opts.signal orelse break :signal null; + if (signal.isUndefined()) { + break :signal null; + } + break :signal try signal.toZig(*AbortSignal); + }, + }, + .capture => |capture| RegisterOptions{ .capture = capture }, + }; + }; + const callback = callback_ orelse return; const em_callback: EventManager.Callback = switch (callback) { @@ -98,14 +130,6 @@ pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventLi .function => |func| .{ .function = func }, }; - const options = blk: { - const o = opts_ orelse break :blk RegisterOptions{}; - break :blk switch (o) { - .options => |opts| opts, - .capture => |capture| RegisterOptions{ .capture = capture }, - }; - }; - switch (exec.js.global) { inline else => |g| _ = try g._event_manager.register(self, typ, em_callback, options), } diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 2cb803324..f8f227516 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -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. +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; } @@ -1013,7 +1079,7 @@ const PostMessageCallback = struct { } }; -const FunctionSetter = union(enum) { +pub const FunctionSetter = union(enum) { func: js.Function.Global, anything: js.Value, }; @@ -1021,7 +1087,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 @@ -1079,6 +1145,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, .{}); diff --git a/src/browser/webapi/collections/HTMLCollection.zig b/src/browser/webapi/collections/HTMLCollection.zig index fa6a71f32..e67395945 100644 --- a/src/browser/webapi/collections/HTMLCollection.zig +++ b/src/browser/webapi/collections/HTMLCollection.zig @@ -212,13 +212,9 @@ pub const JsApi = struct { configurable: bool, }; - fn getIndexes(self: *HTMLCollection, exec: *const Execution) !js.Array { - const frame = switch (exec.js.global) { - .frame => |f| f, - .worker => unreachable, - }; + fn getIndexes(self: *HTMLCollection, frame: *Frame) !js.Array { const len = self.length(frame); - var arr = exec.js.local.?.newArray(len); + var arr = frame.js.local.?.newArray(len); for (0..len) |i| { _ = try arr.set(@intCast(i), i, .{}); } diff --git a/src/browser/webapi/element/html/Body.zig b/src/browser/webapi/element/html/Body.zig index faed2adf8..4101261cd 100644 --- a/src/browser/webapi/element/html/Body.zig +++ b/src/browser/webapi/element/html/Body.zig @@ -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,14 +38,57 @@ 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. + +// The aliased Window is the one of the element's node document's frame — not +// the caller's frame, which differs when a same-origin script reaches into +// another frame (e.g. the parent setting iframeDoc.body.onblur). +fn reflectedWindow(self: *Body, frame: *Frame) *Window { + return self.asElement().ownerFrame(frame).window; } -/// 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 getOnBlur(self: *Body, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_blur; +} +pub fn setOnBlur(self: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_blur = Window.getFunctionFromSetter(setter); +} + +pub fn getOnError(self: *Body, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_error; +} +pub fn setOnError(self: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_error = Window.getFunctionFromSetter(setter); +} + +pub fn getOnFocus(self: *Body, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_focus; +} +pub fn setOnFocus(self: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_focus = Window.getFunctionFromSetter(setter); +} + +pub fn getOnLoad(self: *Body, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_load; +} +pub fn setOnLoad(self: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_load = Window.getFunctionFromSetter(setter); +} + +pub fn getOnResize(self: *Body, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_resize; +} +pub fn setOnResize(self: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_resize = Window.getFunctionFromSetter(setter); +} + +pub fn getOnScroll(self: *Body, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_scroll; +} +pub fn setOnScroll(self: *Body, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_scroll = Window.getFunctionFromSetter(setter); } pub const JsApi = struct { @@ -56,17 +100,36 @@ 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 }); + const owner = node.ownerFrame(frame); + inline for (window_reflecting_attributes) |attr| { + if (el.getAttributeSafe(comptime .wrap(attr))) |value| { + owner.window.setWindowReflectingHandlerFromAttribute(comptime .wrap(attr), value, owner); + } } } + + pub fn attributeChange(el: *Element, name: String, value: String, frame: *Frame) !void { + const owner = el.ownerFrame(frame); + owner.window.setWindowReflectingHandlerFromAttribute(name, value.str(), owner); + } + + pub fn attributeRemove(el: *Element, name: String, frame: *Frame) !void { + const owner = el.ownerFrame(frame); + owner.window.setWindowReflectingHandlerFromAttribute(name, null, owner); + } }; diff --git a/src/browser/webapi/element/html/FrameSet.zig b/src/browser/webapi/element/html/FrameSet.zig index 4e84a25c3..2643580a4 100644 --- a/src/browser/webapi/element/html/FrameSet.zig +++ b/src/browser/webapi/element/html/FrameSet.zig @@ -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,59 @@ 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. + +// The aliased Window is the one of the element's node document's frame — not +// the caller's frame, which differs when a same-origin script reaches into +// another frame (e.g. the parent setting a handler on a child frameset). +fn reflectedWindow(self: *FrameSet, frame: *Frame) *Window { + return self.asElement().ownerFrame(frame).window; +} + +pub fn getOnBlur(self: *FrameSet, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_blur; +} +pub fn setOnBlur(self: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_blur = Window.getFunctionFromSetter(setter); +} + +pub fn getOnError(self: *FrameSet, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_error; +} +pub fn setOnError(self: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_error = Window.getFunctionFromSetter(setter); +} + +pub fn getOnFocus(self: *FrameSet, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_focus; +} +pub fn setOnFocus(self: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_focus = Window.getFunctionFromSetter(setter); +} + +pub fn getOnLoad(self: *FrameSet, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_load; +} +pub fn setOnLoad(self: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_load = Window.getFunctionFromSetter(setter); +} + +pub fn getOnResize(self: *FrameSet, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_resize; +} +pub fn setOnResize(self: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_resize = Window.getFunctionFromSetter(setter); +} + +pub fn getOnScroll(self: *FrameSet, frame: *Frame) ?js.Function.Global { + return self.reflectedWindow(frame)._on_scroll; +} +pub fn setOnScroll(self: *FrameSet, setter: ?Window.FunctionSetter, frame: *Frame) !void { + self.reflectedWindow(frame)._on_scroll = Window.getFunctionFromSetter(setter); +} + pub fn getCols(self: *FrameSet) []const u8 { return self.asElement().getAttributeSafe(comptime .wrap("cols")) orelse ""; } @@ -42,6 +100,39 @@ 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); + const owner = node.ownerFrame(frame); + inline for (window_reflecting_attributes) |attr| { + if (el.getAttributeSafe(comptime .wrap(attr))) |value| { + owner.window.setWindowReflectingHandlerFromAttribute(comptime .wrap(attr), value, owner); + } + } + } + + pub fn attributeChange(el: *Element, name: String, value: String, frame: *Frame) !void { + const owner = el.ownerFrame(frame); + owner.window.setWindowReflectingHandlerFromAttribute(name, value.str(), owner); + } + + pub fn attributeRemove(el: *Element, name: String, frame: *Frame) !void { + const owner = el.ownerFrame(frame); + owner.window.setWindowReflectingHandlerFromAttribute(name, null, owner); + } }; const testing = @import("../../../../testing.zig");