mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 07:03:29 +00:00
Merge pull request #156 from lightpanda-io/events
First implementation of Event system
This commit is contained in:
@@ -4,6 +4,7 @@ const Console = @import("jsruntime").Console;
|
||||
|
||||
const DOM = @import("dom/dom.zig");
|
||||
const HTML = @import("html/html.zig");
|
||||
const Events = @import("events/event.zig");
|
||||
|
||||
pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||
|
||||
@@ -11,5 +12,6 @@ pub const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
Console,
|
||||
DOM.Interfaces,
|
||||
Events.Interfaces,
|
||||
HTML.Interfaces,
|
||||
});
|
||||
|
||||
@@ -81,6 +81,15 @@ pub const Document = struct {
|
||||
return try parser.documentGetDoctype(self);
|
||||
}
|
||||
|
||||
pub fn _createEvent(_: *parser.Document, eventCstr: []const u8) !*parser.Event {
|
||||
// TODO: for now only "Event" constructor is supported
|
||||
// see table on https://dom.spec.whatwg.org/#dom-document-createevent $2
|
||||
if (std.ascii.eqlIgnoreCase(eventCstr, "Event") or std.ascii.eqlIgnoreCase(eventCstr, "Events")) {
|
||||
return try parser.eventCreate();
|
||||
}
|
||||
return parser.DOMError.NotSupported;
|
||||
}
|
||||
|
||||
pub fn _getElementById(self: *parser.Document, id: []const u8) !?ElementUnion {
|
||||
const e = try parser.documentGetElementById(self, id) orelse return null;
|
||||
return try Element.toInterface(e);
|
||||
|
||||
@@ -1,9 +1,223 @@
|
||||
const std = @import("std");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
const JSObjectID = jsruntime.JSObjectID;
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const DOMException = @import("exceptions.zig").DOMException;
|
||||
const Nod = @import("node.zig");
|
||||
|
||||
// EventTarget interfaces
|
||||
pub const Union = Nod.Union;
|
||||
|
||||
// EventTarget implementation
|
||||
pub const EventTarget = struct {
|
||||
pub const Self = parser.EventTarget;
|
||||
pub const Exception = DOMException;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub fn toInterface(et: *parser.EventTarget) !Union {
|
||||
// NOTE: for now we state that all EventTarget are Nodes
|
||||
// TODO: handle other types (eg. Window)
|
||||
return Nod.Node.toInterface(@as(*parser.Node, @ptrCast(et)));
|
||||
}
|
||||
|
||||
// JS funcs
|
||||
// --------
|
||||
|
||||
pub fn _addEventListener(
|
||||
self: *parser.EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
eventType: []const u8,
|
||||
cbk: Callback,
|
||||
capture: ?bool,
|
||||
// TODO: hanle EventListenerOptions
|
||||
// see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
) !void {
|
||||
|
||||
// check if event target has already this listener
|
||||
const lst = try parser.eventTargetHasListener(
|
||||
self,
|
||||
eventType,
|
||||
capture orelse false,
|
||||
cbk.id(),
|
||||
);
|
||||
if (lst != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try parser.eventTargetAddEventListener(
|
||||
self,
|
||||
alloc,
|
||||
eventType,
|
||||
cbk,
|
||||
capture orelse false,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _removeEventListener(
|
||||
self: *parser.EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
eventType: []const u8,
|
||||
cbk_id: JSObjectID,
|
||||
capture: ?bool,
|
||||
// TODO: hanle EventListenerOptions
|
||||
// see #https://github.com/lightpanda-io/jsruntime-lib/issues/114
|
||||
) !void {
|
||||
|
||||
// check if event target has already this listener
|
||||
const lst = try parser.eventTargetHasListener(
|
||||
self,
|
||||
eventType,
|
||||
capture orelse false,
|
||||
cbk_id.get(),
|
||||
);
|
||||
if (lst == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
// remove listener
|
||||
try parser.eventTargetRemoveEventListener(
|
||||
self,
|
||||
alloc,
|
||||
eventType,
|
||||
lst.?,
|
||||
capture orelse false,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn _dispatchEvent(self: *parser.EventTarget, event: *parser.Event) !bool {
|
||||
return try parser.eventTargetDispatchEvent(self, event);
|
||||
}
|
||||
|
||||
pub fn deinit(self: *parser.EventTarget, alloc: std.mem.Allocator) void {
|
||||
parser.eventTargetRemoveAllEventListeners(self, alloc) catch unreachable;
|
||||
}
|
||||
};
|
||||
|
||||
// Tests
|
||||
// -----
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var common = [_]Case{
|
||||
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "let para = document.getElementById('para')", .ex = "undefined" },
|
||||
// NOTE: as some event properties will change during the event dispatching phases
|
||||
// we need to copy thoses values in order to check them afterwards
|
||||
.{ .src =
|
||||
\\var nb = 0; var evt; var phase; var cur;
|
||||
\\function cbk(event) {
|
||||
\\evt = event;
|
||||
\\phase = event.eventPhase;
|
||||
\\cur = event.currentTarget;
|
||||
\\nb ++;
|
||||
\\}
|
||||
, .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &common);
|
||||
|
||||
var basic = [_]Case{
|
||||
.{ .src = "content.addEventListener('basic', cbk)", .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
.{ .src = "evt instanceof Event", .ex = "true" },
|
||||
.{ .src = "evt.type", .ex = "basic" },
|
||||
.{ .src = "phase", .ex = "2" },
|
||||
.{ .src = "cur.getAttribute('id')", .ex = "content" },
|
||||
};
|
||||
try checkCases(js_env, &basic);
|
||||
|
||||
var basic_child = [_]Case{
|
||||
.{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" },
|
||||
.{ .src = "para.dispatchEvent(new Event('basic'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "0" }, // handler is not called, no capture, not the target, no bubbling
|
||||
.{ .src = "evt === undefined", .ex = "true" },
|
||||
};
|
||||
try checkCases(js_env, &basic_child);
|
||||
|
||||
var basic_twice = [_]Case{
|
||||
.{ .src = "nb = 0", .ex = "0" },
|
||||
.{ .src = "content.addEventListener('basic', cbk)", .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
};
|
||||
try checkCases(js_env, &basic_twice);
|
||||
|
||||
var basic_twice_capture = [_]Case{
|
||||
.{ .src = "nb = 0", .ex = "0" },
|
||||
.{ .src = "content.addEventListener('basic', cbk, true)", .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "2" },
|
||||
};
|
||||
try checkCases(js_env, &basic_twice_capture);
|
||||
|
||||
var basic_remove = [_]Case{
|
||||
.{ .src = "nb = 0", .ex = "0" },
|
||||
.{ .src = "content.removeEventListener('basic', cbk)", .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
};
|
||||
try checkCases(js_env, &basic_remove);
|
||||
|
||||
var basic_capture_remove = [_]Case{
|
||||
.{ .src = "nb = 0", .ex = "0" },
|
||||
.{ .src = "content.removeEventListener('basic', cbk, true)", .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('basic'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "0" },
|
||||
};
|
||||
try checkCases(js_env, &basic_capture_remove);
|
||||
|
||||
var capture = [_]Case{
|
||||
.{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" },
|
||||
.{ .src = "content.addEventListener('capture', cbk, true)", .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('capture'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
.{ .src = "evt instanceof Event", .ex = "true" },
|
||||
.{ .src = "evt.type", .ex = "capture" },
|
||||
.{ .src = "phase", .ex = "2" },
|
||||
.{ .src = "cur.getAttribute('id')", .ex = "content" },
|
||||
};
|
||||
try checkCases(js_env, &capture);
|
||||
|
||||
var capture_child = [_]Case{
|
||||
.{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" },
|
||||
.{ .src = "para.dispatchEvent(new Event('capture'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
.{ .src = "evt instanceof Event", .ex = "true" },
|
||||
.{ .src = "evt.type", .ex = "capture" },
|
||||
.{ .src = "phase", .ex = "1" },
|
||||
.{ .src = "cur.getAttribute('id')", .ex = "content" },
|
||||
};
|
||||
try checkCases(js_env, &capture_child);
|
||||
|
||||
var bubbles = [_]Case{
|
||||
.{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" },
|
||||
.{ .src = "content.addEventListener('bubbles', cbk)", .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('bubbles', {bubbles: true}))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
.{ .src = "evt instanceof Event", .ex = "true" },
|
||||
.{ .src = "evt.type", .ex = "bubbles" },
|
||||
.{ .src = "evt.bubbles", .ex = "true" },
|
||||
.{ .src = "phase", .ex = "2" },
|
||||
.{ .src = "cur.getAttribute('id')", .ex = "content" },
|
||||
};
|
||||
try checkCases(js_env, &bubbles);
|
||||
|
||||
var bubbles_child = [_]Case{
|
||||
.{ .src = "nb = 0; evt = undefined; phase = undefined; cur = undefined", .ex = "undefined" },
|
||||
.{ .src = "para.dispatchEvent(new Event('bubbles', {bubbles: true}))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
.{ .src = "evt instanceof Event", .ex = "true" },
|
||||
.{ .src = "evt.type", .ex = "bubbles" },
|
||||
.{ .src = "phase", .ex = "3" },
|
||||
.{ .src = "cur.getAttribute('id')", .ex = "content" },
|
||||
};
|
||||
try checkCases(js_env, &bubbles_child);
|
||||
}
|
||||
|
||||
201
src/events/event.zig
Normal file
201
src/events/event.zig
Normal file
@@ -0,0 +1,201 @@
|
||||
const std = @import("std");
|
||||
|
||||
const generate = @import("../generate.zig");
|
||||
|
||||
const jsruntime = @import("jsruntime");
|
||||
const Callback = jsruntime.Callback;
|
||||
const Case = jsruntime.test_utils.Case;
|
||||
const checkCases = jsruntime.test_utils.checkCases;
|
||||
|
||||
const parser = @import("../netsurf.zig");
|
||||
|
||||
const DOMException = @import("../dom/exceptions.zig").DOMException;
|
||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||
const EventTargetUnion = @import("../dom/event_target.zig").Union;
|
||||
|
||||
// https://dom.spec.whatwg.org/#event
|
||||
pub const Event = struct {
|
||||
pub const Self = parser.Event;
|
||||
pub const Exception = DOMException;
|
||||
pub const mem_guarantied = true;
|
||||
|
||||
pub const EventInit = parser.EventInit;
|
||||
|
||||
// JS
|
||||
// --
|
||||
|
||||
pub const _CAPTURING_PHASE = 1;
|
||||
pub const _AT_TARGET = 2;
|
||||
pub const _BUBBLING_PHASE = 3;
|
||||
|
||||
pub fn constructor(eventType: []const u8, opts: ?EventInit) !*parser.Event {
|
||||
const event = try parser.eventCreate();
|
||||
try parser.eventInit(event, eventType, opts orelse EventInit{});
|
||||
return event;
|
||||
}
|
||||
|
||||
// Getters
|
||||
|
||||
pub fn get_type(self: *parser.Event) ![]const u8 {
|
||||
return try parser.eventType(self);
|
||||
}
|
||||
|
||||
pub fn get_target(self: *parser.Event) !?EventTargetUnion {
|
||||
const et = try parser.eventTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?);
|
||||
}
|
||||
|
||||
pub fn get_currentTarget(self: *parser.Event) !?EventTargetUnion {
|
||||
const et = try parser.eventCurrentTarget(self);
|
||||
if (et == null) return null;
|
||||
return try EventTarget.toInterface(et.?);
|
||||
}
|
||||
|
||||
pub fn get_eventPhase(self: *parser.Event) !u8 {
|
||||
return try parser.eventPhase(self);
|
||||
}
|
||||
|
||||
pub fn get_bubbles(self: *parser.Event) !bool {
|
||||
return try parser.eventBubbles(self);
|
||||
}
|
||||
|
||||
pub fn get_cancelable(self: *parser.Event) !bool {
|
||||
return try parser.eventCancelable(self);
|
||||
}
|
||||
|
||||
pub fn get_defaultPrevented(self: *parser.Event) !bool {
|
||||
return try parser.eventDefaultPrevented(self);
|
||||
}
|
||||
|
||||
pub fn get_isTrusted(self: *parser.Event) !bool {
|
||||
return try parser.eventIsTrusted(self);
|
||||
}
|
||||
|
||||
pub fn get_timestamp(self: *parser.Event) !u32 {
|
||||
return try parser.eventTimestamp(self);
|
||||
}
|
||||
|
||||
// Methods
|
||||
|
||||
pub fn _initEvent(
|
||||
self: *parser.Event,
|
||||
eventType: []const u8,
|
||||
bubbles: ?bool,
|
||||
cancelable: ?bool,
|
||||
) !void {
|
||||
const opts = EventInit{
|
||||
.bubbles = bubbles orelse false,
|
||||
.cancelable = cancelable orelse false,
|
||||
};
|
||||
return try parser.eventInit(self, eventType, opts);
|
||||
}
|
||||
|
||||
pub fn _stopPropagation(self: *parser.Event) !void {
|
||||
return try parser.eventStopPropagation(self);
|
||||
}
|
||||
|
||||
pub fn _stopImmediatePropagation(self: *parser.Event) !void {
|
||||
return try parser.eventStopImmediatePropagation(self);
|
||||
}
|
||||
|
||||
pub fn _preventDefault(self: *parser.Event) !void {
|
||||
return try parser.eventPreventDefault(self);
|
||||
}
|
||||
};
|
||||
|
||||
// Event interfaces
|
||||
pub const Interfaces = generate.Tuple(.{
|
||||
Event,
|
||||
});
|
||||
const Generated = generate.Union.compile(Interfaces);
|
||||
pub const Union = Generated._union;
|
||||
|
||||
pub fn testExecFn(
|
||||
_: std.mem.Allocator,
|
||||
js_env: *jsruntime.Env,
|
||||
) anyerror!void {
|
||||
var common = [_]Case{
|
||||
.{ .src = "let content = document.getElementById('content')", .ex = "undefined" },
|
||||
.{ .src = "let para = document.getElementById('para')", .ex = "undefined" },
|
||||
.{ .src = "var nb = 0; var evt", .ex = "undefined" },
|
||||
};
|
||||
try checkCases(js_env, &common);
|
||||
|
||||
var basic = [_]Case{
|
||||
.{ .src =
|
||||
\\content.addEventListener('target',
|
||||
\\function(e) {
|
||||
\\evt = e; nb = nb + 1;
|
||||
\\e.preventDefault();
|
||||
\\})
|
||||
, .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('target', {bubbles: true, cancelable: true}))", .ex = "false" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
.{ .src = "evt.target === content", .ex = "true" },
|
||||
.{ .src = "evt.bubbles", .ex = "true" },
|
||||
.{ .src = "evt.cancelable", .ex = "true" },
|
||||
.{ .src = "evt.defaultPrevented", .ex = "true" },
|
||||
.{ .src = "evt.isTrusted", .ex = "true" },
|
||||
.{ .src = "evt.timestamp > 1704063600", .ex = "true" }, // 2024/01/01 00:00
|
||||
// event.type, event.currentTarget, event.phase checked in EventTarget
|
||||
};
|
||||
try checkCases(js_env, &basic);
|
||||
|
||||
var stop = [_]Case{
|
||||
.{ .src = "nb = 0", .ex = "0" },
|
||||
.{ .src =
|
||||
\\content.addEventListener('stop',
|
||||
\\function(e) {
|
||||
\\e.stopPropagation();
|
||||
\\nb = nb + 1;
|
||||
\\}, true)
|
||||
, .ex = "undefined" },
|
||||
// the following event listener will not be invoked
|
||||
.{ .src =
|
||||
\\para.addEventListener('stop',
|
||||
\\function(e) {
|
||||
\\nb = nb + 1;
|
||||
\\})
|
||||
, .ex = "undefined" },
|
||||
.{ .src = "para.dispatchEvent(new Event('stop'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at content event listener
|
||||
};
|
||||
try checkCases(js_env, &stop);
|
||||
|
||||
var stop_immediate = [_]Case{
|
||||
.{ .src = "nb = 0", .ex = "0" },
|
||||
.{ .src =
|
||||
\\content.addEventListener('immediate',
|
||||
\\function(e) {
|
||||
\\e.stopImmediatePropagation();
|
||||
\\nb = nb + 1;
|
||||
\\})
|
||||
, .ex = "undefined" },
|
||||
// the following event listener will not be invoked
|
||||
.{ .src =
|
||||
\\content.addEventListener('immediate',
|
||||
\\function(e) {
|
||||
\\nb = nb + 1;
|
||||
\\})
|
||||
, .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(new Event('immediate'))", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" }, // will be 2 if event was not stopped at first content event listener
|
||||
};
|
||||
try checkCases(js_env, &stop_immediate);
|
||||
|
||||
var legacy = [_]Case{
|
||||
.{ .src = "nb = 0", .ex = "0" },
|
||||
.{ .src =
|
||||
\\content.addEventListener('legacy',
|
||||
\\function(e) {
|
||||
\\evt = e; nb = nb + 1;
|
||||
\\})
|
||||
, .ex = "undefined" },
|
||||
.{ .src = "let evtLegacy = document.createEvent('Event')", .ex = "undefined" },
|
||||
.{ .src = "evtLegacy.initEvent('legacy')", .ex = "undefined" },
|
||||
.{ .src = "content.dispatchEvent(evtLegacy)", .ex = "true" },
|
||||
.{ .src = "nb", .ex = "1" },
|
||||
};
|
||||
try checkCases(js_env, &legacy);
|
||||
}
|
||||
278
src/netsurf.zig
278
src/netsurf.zig
@@ -5,6 +5,8 @@ const c = @cImport({
|
||||
@cInclude("dom/bindings/hubbub/parser.h");
|
||||
});
|
||||
|
||||
const Callback = @import("jsruntime").Callback;
|
||||
|
||||
// Vtable
|
||||
// ------
|
||||
|
||||
@@ -341,9 +343,285 @@ fn DOMErr(except: DOMException) DOMError!void {
|
||||
};
|
||||
}
|
||||
|
||||
// Event
|
||||
pub const Event = c.dom_event;
|
||||
|
||||
pub fn eventCreate() !*Event {
|
||||
var evt: ?*Event = undefined;
|
||||
const err = c._dom_event_create(&evt);
|
||||
try DOMErr(err);
|
||||
return evt.?;
|
||||
}
|
||||
|
||||
pub const EventInit = struct {
|
||||
bubbles: bool = false,
|
||||
cancelable: bool = false,
|
||||
composed: bool = false,
|
||||
};
|
||||
|
||||
pub fn eventInit(evt: *Event, typ: []const u8, opts: EventInit) !void {
|
||||
const s = try strFromData(typ);
|
||||
const err = c._dom_event_init(evt, s, opts.bubbles, opts.cancelable);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn eventType(evt: *Event) ![]const u8 {
|
||||
var s: ?*String = undefined;
|
||||
const err = c._dom_event_get_type(evt, &s);
|
||||
try DOMErr(err);
|
||||
return strToData(s.?);
|
||||
}
|
||||
|
||||
pub fn eventTarget(evt: *Event) !?*EventTarget {
|
||||
var et: ?*EventTarget = undefined;
|
||||
const err = c._dom_event_get_target(evt, &et);
|
||||
try DOMErr(err);
|
||||
return et;
|
||||
}
|
||||
|
||||
pub fn eventCurrentTarget(evt: *Event) !?*EventTarget {
|
||||
var et: ?*EventTarget = undefined;
|
||||
const err = c._dom_event_get_current_target(evt, &et);
|
||||
try DOMErr(err);
|
||||
return et;
|
||||
}
|
||||
|
||||
pub fn eventPhase(evt: *Event) !u8 {
|
||||
var phase: c.dom_event_flow_phase = undefined;
|
||||
const err = c._dom_event_get_event_phase(evt, &phase);
|
||||
try DOMErr(err);
|
||||
return @as(u8, @intCast(phase));
|
||||
}
|
||||
|
||||
pub fn eventBubbles(evt: *Event) !bool {
|
||||
var res: bool = undefined;
|
||||
const err = c._dom_event_get_bubbles(evt, &res);
|
||||
try DOMErr(err);
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn eventCancelable(evt: *Event) !bool {
|
||||
var res: bool = undefined;
|
||||
const err = c._dom_event_get_cancelable(evt, &res);
|
||||
try DOMErr(err);
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn eventDefaultPrevented(evt: *Event) !bool {
|
||||
var res: bool = undefined;
|
||||
const err = c._dom_event_is_default_prevented(evt, &res);
|
||||
try DOMErr(err);
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn eventIsTrusted(evt: *Event) !bool {
|
||||
var res: bool = undefined;
|
||||
const err = c._dom_event_get_is_trusted(evt, &res);
|
||||
try DOMErr(err);
|
||||
return res;
|
||||
}
|
||||
|
||||
pub fn eventTimestamp(evt: *Event) !u32 {
|
||||
var ts: c_uint = undefined;
|
||||
const err = c._dom_event_get_timestamp(evt, &ts);
|
||||
try DOMErr(err);
|
||||
return @as(u32, @intCast(ts));
|
||||
}
|
||||
|
||||
pub fn eventStopPropagation(evt: *Event) !void {
|
||||
const err = c._dom_event_stop_propagation(evt);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn eventStopImmediatePropagation(evt: *Event) !void {
|
||||
const err = c._dom_event_stop_immediate_propagation(evt);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn eventPreventDefault(evt: *Event) !void {
|
||||
const err = c._dom_event_prevent_default(evt);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
// EventHandler
|
||||
fn event_handler_cbk(data: *anyopaque) *Callback {
|
||||
const ptr: *align(@alignOf(*Callback)) anyopaque = @alignCast(data);
|
||||
return @as(*Callback, @ptrCast(ptr));
|
||||
}
|
||||
|
||||
const event_handler = struct {
|
||||
fn handle(event: ?*Event, data: ?*anyopaque) callconv(.C) void {
|
||||
if (data) |d| {
|
||||
const func = event_handler_cbk(d);
|
||||
func.call(.{event}) catch unreachable;
|
||||
// NOTE: we can not call func.deinit here
|
||||
// b/c the handler can be called several times
|
||||
// either on this dispatch event or in anoter one
|
||||
}
|
||||
}
|
||||
}.handle;
|
||||
|
||||
// EventListener
|
||||
pub const EventListener = c.dom_event_listener;
|
||||
const EventListenerEntry = c.listener_entry;
|
||||
|
||||
fn eventListenerGetData(lst: *EventListener) ?*anyopaque {
|
||||
return c.dom_event_listener_get_data(lst);
|
||||
}
|
||||
|
||||
// EventTarget
|
||||
pub const EventTarget = c.dom_event_target;
|
||||
|
||||
fn eventTargetVtable(et: *EventTarget) c.dom_event_target_vtable {
|
||||
return getVtable(c.dom_event_target_vtable, EventTarget, et);
|
||||
}
|
||||
|
||||
pub fn eventTargetHasListener(
|
||||
et: *EventTarget,
|
||||
typ: []const u8,
|
||||
capture: bool,
|
||||
cbk_id: usize,
|
||||
) !?*EventListener {
|
||||
const str = try strFromData(typ);
|
||||
|
||||
var current: ?*EventListenerEntry = null;
|
||||
var next: ?*EventListenerEntry = undefined;
|
||||
var lst: ?*EventListener = undefined;
|
||||
|
||||
// iterate over the EventTarget's listeners
|
||||
while (true) {
|
||||
const err = eventTargetVtable(et).iter_event_listener.?(
|
||||
et,
|
||||
str,
|
||||
capture,
|
||||
current,
|
||||
&next,
|
||||
&lst,
|
||||
);
|
||||
try DOMErr(err);
|
||||
|
||||
if (lst) |listener| {
|
||||
// the EventTarget has a listener for this event type
|
||||
// and capture property,
|
||||
// let's check if the callback handler is the same
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
const data = eventListenerGetData(listener);
|
||||
if (data) |d| {
|
||||
const cbk = event_handler_cbk(d);
|
||||
if (cbk_id == cbk.id()) {
|
||||
return lst;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (next == null) {
|
||||
// no more listeners, end of the iteration
|
||||
break;
|
||||
}
|
||||
|
||||
// next iteration
|
||||
current = next;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
pub fn eventTargetAddEventListener(
|
||||
et: *EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
typ: []const u8,
|
||||
cbk: Callback,
|
||||
capture: bool,
|
||||
) !void {
|
||||
// this allocation will be removed either on
|
||||
// eventTargetRemoveEventListener or eventTargetRemoveAllEventListeners
|
||||
const cbk_ptr = try alloc.create(Callback);
|
||||
cbk_ptr.* = cbk;
|
||||
|
||||
const ctx = @as(*anyopaque, @ptrCast(cbk_ptr));
|
||||
var listener: ?*EventListener = undefined;
|
||||
const errLst = c.dom_event_listener_create(event_handler, ctx, &listener);
|
||||
try DOMErr(errLst);
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
|
||||
const s = try strFromData(typ);
|
||||
const err = eventTargetVtable(et).add_event_listener.?(et, s, listener, capture);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn eventTargetRemoveEventListener(
|
||||
et: *EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
typ: []const u8,
|
||||
lst: *EventListener,
|
||||
capture: bool,
|
||||
) !void {
|
||||
const data = eventListenerGetData(lst);
|
||||
// free cbk allocation made on eventTargetAddEventListener
|
||||
if (data) |d| {
|
||||
const cbk_ptr = event_handler_cbk(d);
|
||||
cbk_ptr.deinit(alloc);
|
||||
alloc.destroy(cbk_ptr);
|
||||
}
|
||||
|
||||
const s = try strFromData(typ);
|
||||
const err = eventTargetVtable(et).remove_event_listener.?(et, s, lst, capture);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
pub fn eventTargetRemoveAllEventListeners(
|
||||
et: *EventTarget,
|
||||
alloc: std.mem.Allocator,
|
||||
) !void {
|
||||
var next: ?*EventListenerEntry = undefined;
|
||||
var lst: ?*EventListener = undefined;
|
||||
|
||||
// iterate over the EventTarget's listeners
|
||||
while (true) {
|
||||
const errIter = eventTargetVtable(et).iter_event_listener.?(
|
||||
et,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
&next,
|
||||
&lst,
|
||||
);
|
||||
try DOMErr(errIter);
|
||||
|
||||
if (lst) |listener| {
|
||||
defer c.dom_event_listener_unref(listener);
|
||||
const data = eventListenerGetData(listener);
|
||||
if (data) |d| {
|
||||
// free cbk allocation made on eventTargetAddEventListener
|
||||
const cbk = event_handler_cbk(d);
|
||||
cbk.deinit(alloc);
|
||||
alloc.destroy(cbk);
|
||||
}
|
||||
const err = eventTargetVtable(et).remove_event_listener.?(
|
||||
et,
|
||||
null,
|
||||
lst,
|
||||
false,
|
||||
);
|
||||
try DOMErr(err);
|
||||
}
|
||||
|
||||
if (next == null) {
|
||||
// no more listeners, end of the iteration
|
||||
break;
|
||||
}
|
||||
|
||||
// next iteration
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eventTargetDispatchEvent(et: *EventTarget, event: *Event) !bool {
|
||||
var res: bool = undefined;
|
||||
const err = eventTargetVtable(et).dispatch_event.?(et, event, &res);
|
||||
try DOMErr(err);
|
||||
return res;
|
||||
}
|
||||
|
||||
// NodeType
|
||||
|
||||
pub const NodeType = enum(u4) {
|
||||
|
||||
@@ -20,6 +20,8 @@ const NamedNodeMapExecFn = @import("dom/namednodemap.zig").testExecFn;
|
||||
const DOMTokenListExecFn = @import("dom/token_list.zig").testExecFn;
|
||||
const NodeListTestExecFn = @import("dom/nodelist.zig").testExecFn;
|
||||
const AttrTestExecFn = @import("dom/attribute.zig").testExecFn;
|
||||
const EventTargetTestExecFn = @import("dom/event_target.zig").testExecFn;
|
||||
const EventTestExecFn = @import("events/event.zig").testExecFn;
|
||||
|
||||
pub const Types = jsruntime.reflect(apiweb.Interfaces);
|
||||
|
||||
@@ -73,6 +75,8 @@ fn testsAllExecFn(
|
||||
DOMTokenListExecFn,
|
||||
NodeListTestExecFn,
|
||||
AttrTestExecFn,
|
||||
EventTargetTestExecFn,
|
||||
EventTestExecFn,
|
||||
};
|
||||
|
||||
inline for (testFns) |testFn| {
|
||||
|
||||
Submodule tests/wpt updated: c9e7658223...a2c7f5a24d
2
vendor/jsruntime-lib
vendored
2
vendor/jsruntime-lib
vendored
Submodule vendor/jsruntime-lib updated: 7de65ccd30...2af9e9b7fc
Reference in New Issue
Block a user