webapi: add BeforeUnload/Storage/DeviceMotion/DeviceOrientation/Touch events

Fixes all 21 failing tests in WPT /dom/nodes/Document-createEvent.https.html
(258/279 -> 279/279), Document-createEvent-touchevent.window.html
(0/3 -> 3/3) and, as a side effect, /dom/events/non-cancelable-when-
passive/synthetic-events-cancelable.html (8/12 -> 12/12).

- Five new event interfaces, following the existing patterns:
  BeforeUnloadEvent (returnValue, no constructor per spec), StorageEvent
  (key/oldValue/newValue/url + initStorageEvent; storageArea is always
  null), DeviceMotionEvent and DeviceOrientationEvent (nullable sensor
  values), and TouchEvent (UIEvent subclass with modifier keys and empty
  touch lists — there is no touch input source).
- document.createEvent maps their aliases to real instances instead of
  plain Events, adds "touchevent", and drops the non-spec pluralized
  aliases CustomEvents/FocusEvents/TextEvents, which must throw
  NotSupportedError (the unit test asserting the old behavior is
  updated).
- The "expose legacy touch event APIs" precondition requires
  ontouchstart & co. to exist: the four touch handlers are added to the
  global event handler set, HTMLElement and Document.

Coverage: /dom/nodes/Document-createEvent.https.html 258/279 ->
279/279, Document-createEvent-touchevent.window.html 0/3 -> 3/3,
synthetic-events-cancelable.html 8/12 -> 12/12. No regressions across
/dom/events; 1002/1002 unit tests pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Francis Bouvier
2026-07-10 22:27:01 +02:00
committed by Karl Seguin
parent 58d8aeb070
commit 7f04ee5f0a
12 changed files with 669 additions and 15 deletions

View File

@@ -1091,6 +1091,11 @@ pub const PageJsApis = flattenTypes(&.{
@import("../webapi/event/PageTransitionEvent.zig"),
@import("../webapi/event/PopStateEvent.zig"),
@import("../webapi/event/HashChangeEvent.zig"),
@import("../webapi/event/BeforeUnloadEvent.zig"),
@import("../webapi/event/StorageEvent.zig"),
@import("../webapi/event/DeviceMotionEvent.zig"),
@import("../webapi/event/DeviceOrientationEvent.zig"),
@import("../webapi/event/TouchEvent.zig"),
@import("../webapi/event/UIEvent.zig"),
@import("../webapi/event/MouseEvent.zig"),
@import("../webapi/event/PointerEvent.zig"),

View File

@@ -75,8 +75,10 @@
const event2 = document.createEvent('CUSTOMEVENT');
testing.expectEqual('', event2.type);
const event3 = document.createEvent('CustomEvents');
testing.expectEqual('', event3.type);
// per spec, the pluralized legacy alias is not supported
let threw = false;
try { document.createEvent('CustomEvents'); } catch (e) { threw = e.name === 'NotSupportedError'; }
testing.expectEqual(true, threw);
}
</script>

View File

@@ -108,6 +108,24 @@ pub fn setOnClick(self: *Document, setter: ?Window.FunctionSetter, frame: *Frame
}
}
// Generates a getter/setter pair backed by the frame's attribute-listener
// map, like onclick above, for other document event handler properties.
fn handlerAccessor(comptime handler: @import("global_event_handlers.zig").Handler) type {
return struct {
pub fn get(self: *Document, frame: *Frame) ?js.Function.Global {
return frame._event_target_attr_listeners.get(.{ .target = self.asEventTarget(), .handler = handler });
}
pub fn set(self: *Document, setter: ?Window.FunctionSetter, frame: *Frame) !void {
if (Window.getFunctionFromSetter(setter)) |cb| {
try frame._event_target_attr_listeners.put(frame.arena, .{ .target = self.asEventTarget(), .handler = handler }, cb);
} else {
_ = frame._event_target_attr_listeners.remove(.{ .target = self.asEventTarget(), .handler = handler });
}
}
};
}
pub const Type = union(enum) {
generic,
html: *HTMLDocument,
@@ -495,7 +513,7 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*
break :blk try Event.init("", null, frame._page);
}
if (std.mem.eql(u8, normalized, "customevent") or std.mem.eql(u8, normalized, "customevents")) {
if (std.mem.eql(u8, normalized, "customevent")) {
const CustomEvent = @import("event/CustomEvent.zig");
break :blk (try CustomEvent.init("", null, frame._page)).asEvent();
}
@@ -535,12 +553,12 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*
break :blk (try UIEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "focusevent") or std.mem.eql(u8, normalized, "focusevents")) {
if (std.mem.eql(u8, normalized, "focusevent")) {
const FocusEvent = @import("event/FocusEvent.zig");
break :blk (try FocusEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "textevent") or std.mem.eql(u8, normalized, "textevents")) {
if (std.mem.eql(u8, normalized, "textevent")) {
const TextEvent = @import("event/TextEvent.zig");
break :blk (try TextEvent.init("", null, frame)).asEvent();
}
@@ -550,16 +568,29 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !*
break :blk (try CompositionEvent.init("", null, frame)).asEvent();
}
// Aliases the spec requires createEvent to support but whose
// interfaces aren't implemented yet: return a plain Event so the
// caller can at least initialize and dispatch it.
if (std.mem.eql(u8, normalized, "beforeunloadevent") or
std.mem.eql(u8, normalized, "devicemotionevent") or
std.mem.eql(u8, normalized, "deviceorientationevent") or
std.mem.eql(u8, normalized, "storageevent"))
{
log.info(.not_implemented, "createEvent interface", .{ .type = event_type });
break :blk try Event.init("", null, frame._page);
if (std.mem.eql(u8, normalized, "beforeunloadevent")) {
const BeforeUnloadEvent = @import("event/BeforeUnloadEvent.zig");
break :blk (try BeforeUnloadEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "devicemotionevent")) {
const DeviceMotionEvent = @import("event/DeviceMotionEvent.zig");
break :blk (try DeviceMotionEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "deviceorientationevent")) {
const DeviceOrientationEvent = @import("event/DeviceOrientationEvent.zig");
break :blk (try DeviceOrientationEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "storageevent")) {
const StorageEvent = @import("event/StorageEvent.zig");
break :blk (try StorageEvent.init("", null, frame)).asEvent();
}
if (std.mem.eql(u8, normalized, "touchevent")) {
const TouchEvent = @import("event/TouchEvent.zig");
break :blk (try TouchEvent.init("", null, frame)).asEvent();
}
return error.NotSupported;
@@ -1409,6 +1440,10 @@ pub const JsApi = struct {
pub const onselectionchange = bridge.accessor(Document.getOnSelectionChange, Document.setOnSelectionChange, .{});
pub const onclick = bridge.accessor(Document.getOnClick, Document.setOnClick, .{});
pub const ontouchstart = bridge.accessor(handlerAccessor(.ontouchstart).get, handlerAccessor(.ontouchstart).set, .{});
pub const ontouchend = bridge.accessor(handlerAccessor(.ontouchend).get, handlerAccessor(.ontouchend).set, .{});
pub const ontouchmove = bridge.accessor(handlerAccessor(.ontouchmove).get, handlerAccessor(.ontouchmove).set, .{});
pub const ontouchcancel = bridge.accessor(handlerAccessor(.ontouchcancel).get, handlerAccessor(.ontouchcancel).set, .{});
pub const URL = bridge.accessor(Document.getURL, null, .{});
pub const location = bridge.accessor(Document.getLocation, Document.setLocation, .{});
pub const documentURI = bridge.accessor(Document.getURL, null, .{});

View File

@@ -84,6 +84,10 @@ pub const Type = union(enum) {
page_transition_event: *@import("event/PageTransitionEvent.zig"),
pop_state_event: *@import("event/PopStateEvent.zig"),
hash_change_event: *@import("event/HashChangeEvent.zig"),
before_unload_event: *@import("event/BeforeUnloadEvent.zig"),
storage_event: *@import("event/StorageEvent.zig"),
device_motion_event: *@import("event/DeviceMotionEvent.zig"),
device_orientation_event: *@import("event/DeviceOrientationEvent.zig"),
ui_event: *@import("event/UIEvent.zig"),
promise_rejection_event: *@import("event/PromiseRejectionEvent.zig"),
submit_event: *@import("event/SubmitEvent.zig"),
@@ -193,6 +197,10 @@ pub fn is(self: *Event, comptime T: type) ?*T {
.page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null,
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
.hash_change_event => |e| return if (T == @import("event/HashChangeEvent.zig")) e else null,
.before_unload_event => |e| return if (T == @import("event/BeforeUnloadEvent.zig")) e else null,
.storage_event => |e| return if (T == @import("event/StorageEvent.zig")) e else null,
.device_motion_event => |e| return if (T == @import("event/DeviceMotionEvent.zig")) e else null,
.device_orientation_event => |e| return if (T == @import("event/DeviceOrientationEvent.zig")) e else null,
.promise_rejection_event => |e| return if (T == @import("event/PromiseRejectionEvent.zig")) e else null,
.submit_event => |e| return if (T == @import("event/SubmitEvent.zig")) e else null,
.form_data_event => |e| return if (T == @import("event/FormDataEvent.zig")) e else null,

View File

@@ -1227,6 +1227,38 @@ pub fn getOnToggle(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
return self.getAttributeFunction(.ontoggle, frame);
}
pub fn setOnTouchCancel(self: *HtmlElement, callback: ?js.Function.Global, frame: *Frame) !void {
return self.setAttributeListener(.ontouchcancel, callback, frame);
}
pub fn getOnTouchCancel(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
return self.getAttributeFunction(.ontouchcancel, frame);
}
pub fn setOnTouchEnd(self: *HtmlElement, callback: ?js.Function.Global, frame: *Frame) !void {
return self.setAttributeListener(.ontouchend, callback, frame);
}
pub fn getOnTouchEnd(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
return self.getAttributeFunction(.ontouchend, frame);
}
pub fn setOnTouchMove(self: *HtmlElement, callback: ?js.Function.Global, frame: *Frame) !void {
return self.setAttributeListener(.ontouchmove, callback, frame);
}
pub fn getOnTouchMove(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
return self.getAttributeFunction(.ontouchmove, frame);
}
pub fn setOnTouchStart(self: *HtmlElement, callback: ?js.Function.Global, frame: *Frame) !void {
return self.setAttributeListener(.ontouchstart, callback, frame);
}
pub fn getOnTouchStart(self: *HtmlElement, frame: *Frame) !?js.Function.Global {
return self.getAttributeFunction(.ontouchstart, frame);
}
pub fn setOnTransitionCancel(self: *HtmlElement, callback: ?js.Function.Global, frame: *Frame) !void {
return self.setAttributeListener(.ontransitioncancel, callback, frame);
}
@@ -1773,6 +1805,10 @@ pub const JsApi = struct {
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 ontouchcancel = bridge.accessor(HtmlElement.getOnTouchCancel, HtmlElement.setOnTouchCancel, .{});
pub const ontouchend = bridge.accessor(HtmlElement.getOnTouchEnd, HtmlElement.setOnTouchEnd, .{});
pub const ontouchmove = bridge.accessor(HtmlElement.getOnTouchMove, HtmlElement.setOnTouchMove, .{});
pub const ontouchstart = bridge.accessor(HtmlElement.getOnTouchStart, HtmlElement.setOnTouchStart, .{});
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, .{});

View File

@@ -0,0 +1,89 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
// https://html.spec.whatwg.org/multipage/browsing-the-web.html#the-beforeunloadevent-interface
const BeforeUnloadEvent = @This();
_proto: *Event,
_return_value: []const u8 = "",
const Options = Event.inheritOptions(BeforeUnloadEvent, struct {});
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*BeforeUnloadEvent {
const arena = try frame.getArena(.tiny, "BeforeUnloadEvent");
errdefer frame.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, _opts, false, frame);
}
pub fn initTrusted(typ: String, _opts: ?Options, frame: *Frame) !*BeforeUnloadEvent {
const arena = try frame.getArena(.tiny, "BeforeUnloadEvent.trusted");
errdefer frame.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, frame);
}
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, frame: *Frame) !*BeforeUnloadEvent {
const opts = _opts orelse Options{};
const event = try frame._factory.event(
arena,
typ,
BeforeUnloadEvent{
._proto = undefined,
},
);
Event.populatePrototypes(event, opts, trusted);
return event;
}
pub fn asEvent(self: *BeforeUnloadEvent) *Event {
return self._proto;
}
pub fn getReturnValue(self: *const BeforeUnloadEvent) []const u8 {
return self._return_value;
}
pub fn setReturnValue(self: *BeforeUnloadEvent, value: []const u8, frame: *Frame) !void {
self._return_value = try frame.dupeString(value);
}
pub const JsApi = struct {
pub const bridge = js.Bridge(BeforeUnloadEvent);
pub const Meta = struct {
pub const name = "BeforeUnloadEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
// Per spec BeforeUnloadEvent has no constructor.
pub const returnValue = bridge.accessor(BeforeUnloadEvent.getReturnValue, BeforeUnloadEvent.setReturnValue, .{});
};

View File

@@ -0,0 +1,96 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
// https://w3c.github.io/deviceorientation/#devicemotionevent
const DeviceMotionEvent = @This();
_proto: *Event,
_interval: f64 = 0,
const DeviceMotionEventOptions = struct {
interval: f64 = 0,
};
const Options = Event.inheritOptions(DeviceMotionEvent, DeviceMotionEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*DeviceMotionEvent {
const arena = try frame.getArena(.tiny, "DeviceMotionEvent");
errdefer frame.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = _opts orelse Options{};
const event = try frame._factory.event(
arena,
type_string,
DeviceMotionEvent{
._proto = undefined,
._interval = opts.interval,
},
);
Event.populatePrototypes(event, opts, false);
return event;
}
pub fn asEvent(self: *DeviceMotionEvent) *Event {
return self._proto;
}
// There is no motion sensor: the acceleration and rotation members are null.
pub fn getAcceleration(_: *const DeviceMotionEvent) ?bool {
return null;
}
pub fn getAccelerationIncludingGravity(_: *const DeviceMotionEvent) ?bool {
return null;
}
pub fn getRotationRate(_: *const DeviceMotionEvent) ?bool {
return null;
}
pub fn getInterval(self: *const DeviceMotionEvent) f64 {
return self._interval;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(DeviceMotionEvent);
pub const Meta = struct {
pub const name = "DeviceMotionEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(DeviceMotionEvent.init, .{});
pub const acceleration = bridge.accessor(DeviceMotionEvent.getAcceleration, null, .{ .null_as_undefined = false });
pub const accelerationIncludingGravity = bridge.accessor(DeviceMotionEvent.getAccelerationIncludingGravity, null, .{ .null_as_undefined = false });
pub const rotationRate = bridge.accessor(DeviceMotionEvent.getRotationRate, null, .{ .null_as_undefined = false });
pub const interval = bridge.accessor(DeviceMotionEvent.getInterval, null, .{});
};

View File

@@ -0,0 +1,104 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
// https://w3c.github.io/deviceorientation/#deviceorientationevent
const DeviceOrientationEvent = @This();
_proto: *Event,
_alpha: ?f64 = null,
_beta: ?f64 = null,
_gamma: ?f64 = null,
_absolute: bool = false,
const DeviceOrientationEventOptions = struct {
alpha: ?f64 = null,
beta: ?f64 = null,
gamma: ?f64 = null,
absolute: bool = false,
};
const Options = Event.inheritOptions(DeviceOrientationEvent, DeviceOrientationEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*DeviceOrientationEvent {
const arena = try frame.getArena(.tiny, "DeviceOrientationEvent");
errdefer frame.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = _opts orelse Options{};
const event = try frame._factory.event(
arena,
type_string,
DeviceOrientationEvent{
._proto = undefined,
._alpha = opts.alpha,
._beta = opts.beta,
._gamma = opts.gamma,
._absolute = opts.absolute,
},
);
Event.populatePrototypes(event, opts, false);
return event;
}
pub fn asEvent(self: *DeviceOrientationEvent) *Event {
return self._proto;
}
pub fn getAlpha(self: *const DeviceOrientationEvent) ?f64 {
return self._alpha;
}
pub fn getBeta(self: *const DeviceOrientationEvent) ?f64 {
return self._beta;
}
pub fn getGamma(self: *const DeviceOrientationEvent) ?f64 {
return self._gamma;
}
pub fn getAbsolute(self: *const DeviceOrientationEvent) bool {
return self._absolute;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(DeviceOrientationEvent);
pub const Meta = struct {
pub const name = "DeviceOrientationEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(DeviceOrientationEvent.init, .{});
pub const alpha = bridge.accessor(DeviceOrientationEvent.getAlpha, null, .{ .null_as_undefined = false });
pub const beta = bridge.accessor(DeviceOrientationEvent.getBeta, null, .{ .null_as_undefined = false });
pub const gamma = bridge.accessor(DeviceOrientationEvent.getGamma, null, .{ .null_as_undefined = false });
pub const absolute = bridge.accessor(DeviceOrientationEvent.getAbsolute, null, .{});
};

View File

@@ -0,0 +1,149 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
// https://html.spec.whatwg.org/multipage/webstorage.html#the-storageevent-interface
const StorageEvent = @This();
_proto: *Event,
_key: ?[]const u8 = null,
_old_value: ?[]const u8 = null,
_new_value: ?[]const u8 = null,
_url: []const u8 = "",
const StorageEventOptions = struct {
key: ?[]const u8 = null,
oldValue: ?[]const u8 = null,
newValue: ?[]const u8 = null,
url: []const u8 = "",
};
const Options = Event.inheritOptions(StorageEvent, StorageEventOptions);
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*StorageEvent {
const arena = try frame.getArena(.tiny, "StorageEvent");
errdefer frame.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, _opts, false, frame);
}
pub fn initTrusted(typ: String, _opts: ?Options, frame: *Frame) !*StorageEvent {
const arena = try frame.getArena(.tiny, "StorageEvent.trusted");
errdefer frame.releaseArena(arena);
return initWithTrusted(arena, typ, _opts, true, frame);
}
fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool, frame: *Frame) !*StorageEvent {
const opts = _opts orelse Options{};
const event = try frame._factory.event(
arena,
typ,
StorageEvent{
._proto = undefined,
._key = if (opts.key) |k| try arena.dupe(u8, k) else null,
._old_value = if (opts.oldValue) |v| try arena.dupe(u8, v) else null,
._new_value = if (opts.newValue) |v| try arena.dupe(u8, v) else null,
._url = try arena.dupe(u8, opts.url),
},
);
Event.populatePrototypes(event, opts, trusted);
return event;
}
pub fn asEvent(self: *StorageEvent) *Event {
return self._proto;
}
pub fn getKey(self: *const StorageEvent) ?[]const u8 {
return self._key;
}
pub fn getOldValue(self: *const StorageEvent) ?[]const u8 {
return self._old_value;
}
pub fn getNewValue(self: *const StorageEvent) ?[]const u8 {
return self._new_value;
}
pub fn getUrl(self: *const StorageEvent) []const u8 {
return self._url;
}
// The initiating Storage object is not tracked; always null.
pub fn getStorageArea(_: *const StorageEvent) ?bool {
return null;
}
pub fn initStorageEvent(
self: *StorageEvent,
typ: []const u8,
bubbles: ?bool,
cancelable: ?bool,
key: ?[]const u8,
old_value: ?[]const u8,
new_value: ?[]const u8,
url: ?[]const u8,
frame: *Frame,
) !void {
const event = self._proto;
if (event._event_phase != .none) {
return;
}
const arena = event._arena;
event._initialized = true;
event._type_string = try String.init(arena, typ, .{});
event._bubbles = bubbles orelse false;
event._cancelable = cancelable orelse false;
self._key = if (key) |k| try arena.dupe(u8, k) else null;
self._old_value = if (old_value) |v| try arena.dupe(u8, v) else null;
self._new_value = if (new_value) |v| try arena.dupe(u8, v) else null;
self._url = if (url) |u| try arena.dupe(u8, u) else "";
_ = frame;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(StorageEvent);
pub const Meta = struct {
pub const name = "StorageEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(StorageEvent.init, .{});
pub const key = bridge.accessor(StorageEvent.getKey, null, .{ .null_as_undefined = false });
pub const oldValue = bridge.accessor(StorageEvent.getOldValue, null, .{ .null_as_undefined = false });
pub const newValue = bridge.accessor(StorageEvent.getNewValue, null, .{ .null_as_undefined = false });
pub const url = bridge.accessor(StorageEvent.getUrl, null, .{});
pub const storageArea = bridge.accessor(StorageEvent.getStorageArea, null, .{ .null_as_undefined = false });
pub const initStorageEvent = bridge.function(StorageEvent.initStorageEvent, .{});
};

View File

@@ -0,0 +1,124 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig");
const Frame = @import("../../Frame.zig");
const Event = @import("../Event.zig");
const UIEvent = @import("UIEvent.zig");
const String = lp.String;
const Allocator = std.mem.Allocator;
// https://w3c.github.io/touch-events/#touchevent-interface
// There is no touch input source: the touch lists are always empty.
const TouchEvent = @This();
_proto: *UIEvent,
_alt_key: bool = false,
_meta_key: bool = false,
_ctrl_key: bool = false,
_shift_key: bool = false,
pub const TouchEventOptions = struct {
altKey: bool = false,
metaKey: bool = false,
ctrlKey: bool = false,
shiftKey: bool = false,
};
pub const Options = Event.inheritOptions(
TouchEvent,
TouchEventOptions,
);
pub fn init(typ: []const u8, _opts: ?Options, frame: *Frame) !*TouchEvent {
const arena = try frame.getArena(.tiny, "TouchEvent");
errdefer frame.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = _opts orelse Options{};
const event = try frame._factory.uiEvent(
arena,
type_string,
TouchEvent{
._proto = undefined,
._alt_key = opts.altKey,
._meta_key = opts.metaKey,
._ctrl_key = opts.ctrlKey,
._shift_key = opts.shiftKey,
},
);
Event.populatePrototypes(event, opts, false);
return event;
}
pub fn asEvent(self: *TouchEvent) *Event {
return self._proto.asEvent();
}
pub fn getTouches(_: *const TouchEvent) []const bool {
return &.{};
}
pub fn getTargetTouches(_: *const TouchEvent) []const bool {
return &.{};
}
pub fn getChangedTouches(_: *const TouchEvent) []const bool {
return &.{};
}
pub fn getAltKey(self: *const TouchEvent) bool {
return self._alt_key;
}
pub fn getMetaKey(self: *const TouchEvent) bool {
return self._meta_key;
}
pub fn getCtrlKey(self: *const TouchEvent) bool {
return self._ctrl_key;
}
pub fn getShiftKey(self: *const TouchEvent) bool {
return self._shift_key;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(TouchEvent);
pub const Meta = struct {
pub const name = "TouchEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
};
pub const constructor = bridge.constructor(TouchEvent.init, .{});
pub const touches = bridge.accessor(TouchEvent.getTouches, null, .{});
pub const targetTouches = bridge.accessor(TouchEvent.getTargetTouches, null, .{});
pub const changedTouches = bridge.accessor(TouchEvent.getChangedTouches, null, .{});
pub const altKey = bridge.accessor(TouchEvent.getAltKey, null, .{});
pub const metaKey = bridge.accessor(TouchEvent.getMetaKey, null, .{});
pub const ctrlKey = bridge.accessor(TouchEvent.getCtrlKey, null, .{});
pub const shiftKey = bridge.accessor(TouchEvent.getShiftKey, null, .{});
};

View File

@@ -41,6 +41,7 @@ pub const Type = union(enum) {
text_event: *@import("TextEvent.zig"),
input_event: *@import("InputEvent.zig"),
composition_event: *@import("CompositionEvent.zig"),
touch_event: *@import("TouchEvent.zig"),
};
pub const UIEventOptions = struct {
@@ -90,6 +91,7 @@ pub fn is(self: *UIEvent, comptime T: type) ?*T {
.text_event => |e| return if (T == @import("TextEvent.zig")) e else null,
.input_event => |e| return if (T == @import("InputEvent.zig")) e else null,
.composition_event => |e| return if (T == @import("CompositionEvent.zig")) e else null,
.touch_event => |e| return if (T == @import("TouchEvent.zig")) e else null,
}
return null;
}

View File

@@ -156,6 +156,10 @@ pub const Handler = enum(u7) {
onsuspend,
ontimeupdate,
ontoggle,
ontouchcancel,
ontouchend,
ontouchmove,
ontouchstart,
ontransitioncancel,
ontransitionend,
ontransitionrun,