mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-12-14 15:28:57 +00:00
Event listener can now be an object with a handleEvent function
This commit is contained in:
@@ -57,7 +57,13 @@ pub const RegisterOptions = struct {
|
||||
passive: bool = false,
|
||||
signal: ?*@import("webapi/AbortSignal.zig") = null,
|
||||
};
|
||||
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, opts: RegisterOptions) !void {
|
||||
|
||||
pub const Callback = union(enum) {
|
||||
function: js.Function,
|
||||
object: js.Object,
|
||||
};
|
||||
|
||||
pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, opts: RegisterOptions) !void {
|
||||
if (comptime IS_DEBUG) {
|
||||
log.debug(.event, "eventManager.register", .{ .type = typ, .capture = opts.capture, .once = opts.once });
|
||||
}
|
||||
@@ -71,11 +77,15 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, func
|
||||
|
||||
const gop = try self.lookup.getOrPut(self.arena, @intFromPtr(target));
|
||||
if (gop.found_existing) {
|
||||
// check for duplicate functions already registered
|
||||
// check for duplicate callbacks already registered
|
||||
var node = gop.value_ptr.first;
|
||||
while (node) |n| {
|
||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||
if (listener.function.eql(function) and listener.capture == opts.capture) {
|
||||
const is_duplicate = switch (callback) {
|
||||
.object => |obj| listener.function.eqlObject(obj),
|
||||
.function => |func| listener.function.eqlFunction(func),
|
||||
};
|
||||
if (is_duplicate and listener.capture == opts.capture) {
|
||||
return;
|
||||
}
|
||||
node = n.next;
|
||||
@@ -84,13 +94,18 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, func
|
||||
gop.value_ptr.* = .{};
|
||||
}
|
||||
|
||||
const func = switch (callback) {
|
||||
.function => |f| Function{ .value = f },
|
||||
.object => |o| Function{ .object = o },
|
||||
};
|
||||
|
||||
const listener = try self.listener_pool.create();
|
||||
listener.* = .{
|
||||
.node = .{},
|
||||
.once = opts.once,
|
||||
.capture = opts.capture,
|
||||
.passive = opts.passive,
|
||||
.function = .{ .value = function },
|
||||
.function = func,
|
||||
.signal = opts.signal,
|
||||
.typ = try String.init(self.arena, typ, .{}),
|
||||
};
|
||||
@@ -98,9 +113,9 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, func
|
||||
gop.value_ptr.append(&listener.node);
|
||||
}
|
||||
|
||||
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, function: js.Function, use_capture: bool) void {
|
||||
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
|
||||
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
|
||||
if (findListener(list, typ, function, use_capture)) |listener| {
|
||||
if (findListener(list, typ, callback, use_capture)) |listener| {
|
||||
self.removeListener(list, listener);
|
||||
}
|
||||
}
|
||||
@@ -119,7 +134,17 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
|
||||
|
||||
switch (target._type) {
|
||||
.node => |node| try self.dispatchNode(node, event, &was_handled),
|
||||
.xhr, .window, .abort_signal, .media_query_list, .message_port, .text_track_cue, .navigation, .screen, .screen_orientation => {
|
||||
.xhr,
|
||||
.window,
|
||||
.abort_signal,
|
||||
.media_query_list,
|
||||
.message_port,
|
||||
.text_track_cue,
|
||||
.navigation,
|
||||
.screen,
|
||||
.screen_orientation,
|
||||
.generic,
|
||||
=> {
|
||||
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
|
||||
try self.dispatchAll(list, target, event, &was_handled);
|
||||
},
|
||||
@@ -304,6 +329,11 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
|
||||
const str = try page.call_arena.dupeZ(u8, string.str());
|
||||
try self.page.js.eval(str, null);
|
||||
},
|
||||
.object => |obj| {
|
||||
if (try obj.getFunction("handleEvent")) |handleEvent| {
|
||||
try handleEvent.callWithThis(void, obj, .{event});
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
// Restore original target (only if we changed it)
|
||||
@@ -350,12 +380,16 @@ fn cleanupMarkedListeners(self: *EventManager, list: *std.DoublyLinkedList) void
|
||||
}
|
||||
}
|
||||
|
||||
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, function: js.Function, capture: bool) ?*Listener {
|
||||
fn findListener(list: *const std.DoublyLinkedList, typ: []const u8, callback: Callback, capture: bool) ?*Listener {
|
||||
var node = list.first;
|
||||
while (node) |n| {
|
||||
node = n.next;
|
||||
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
|
||||
if (!listener.function.eql(function)) {
|
||||
const matches = switch (callback) {
|
||||
.object => |obj| listener.function.eqlObject(obj),
|
||||
.function => |func| listener.function.eqlFunction(func),
|
||||
};
|
||||
if (!matches) {
|
||||
continue;
|
||||
}
|
||||
if (listener.capture != capture) {
|
||||
@@ -383,11 +417,19 @@ const Listener = struct {
|
||||
const Function = union(enum) {
|
||||
value: js.Function,
|
||||
string: String,
|
||||
object: js.Object,
|
||||
|
||||
fn eql(self: Function, func: js.Function) bool {
|
||||
fn eqlFunction(self: Function, func: js.Function) bool {
|
||||
return switch (self) {
|
||||
.string => false,
|
||||
.value => |v| return v.id == func.id,
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
|
||||
fn eqlObject(self: Function, obj: js.Object) bool {
|
||||
return switch (self) {
|
||||
.object => |o| return o.getId() == obj.getId(),
|
||||
else => false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -32,6 +32,10 @@ const Object = @This();
|
||||
js_obj: v8.Object,
|
||||
context: *js.Context,
|
||||
|
||||
pub fn getId(self: Object) u32 {
|
||||
return self.js_obj.getIdentityHash();
|
||||
}
|
||||
|
||||
pub const SetOpts = packed struct(u32) {
|
||||
READ_ONLY: bool = false,
|
||||
DONT_ENUM: bool = false,
|
||||
|
||||
@@ -497,3 +497,118 @@
|
||||
testing.expectEqual('inner1', nested_calls[4]);
|
||||
testing.expectEqual(5, nested_calls.length);
|
||||
</script>
|
||||
|
||||
<div id="content"><p id=para></p></div>
|
||||
<script id=event_target>
|
||||
{
|
||||
testing.expectEqual('[object EventTarget]', new EventTarget().toString());
|
||||
|
||||
let content = $('#content');
|
||||
let para = $('#para');
|
||||
|
||||
var nb = 0;
|
||||
var evt;
|
||||
var phase;
|
||||
var cur;
|
||||
|
||||
function reset() {
|
||||
nb = 0;
|
||||
evt = undefined;
|
||||
phase = undefined;
|
||||
cur = undefined;
|
||||
}
|
||||
|
||||
function cbk(event) {
|
||||
evt = event;
|
||||
phase = event.eventPhase;
|
||||
cur = event.currentTarget;
|
||||
nb++;
|
||||
}
|
||||
|
||||
content.addEventListener('basic', cbk);
|
||||
content.dispatchEvent(new Event('basic'));
|
||||
testing.expectEqual(1, nb);
|
||||
testing.expectEqual(true, evt instanceof Event);
|
||||
testing.expectEqual('basic', evt.type);
|
||||
testing.expectEqual(2, phase);
|
||||
testing.expectEqual('content', cur.getAttribute('id'));
|
||||
|
||||
reset();
|
||||
para.dispatchEvent(new Event('basic'))
|
||||
|
||||
// handler is not called, no capture, not the targeno bubbling
|
||||
testing.expectEqual(0, nb);
|
||||
testing.expectEqual(undefined, evt);
|
||||
|
||||
reset();
|
||||
content.addEventListener('basic', cbk);
|
||||
content.dispatchEvent(new Event('basic'))
|
||||
testing.expectEqual(1, nb);
|
||||
|
||||
reset();
|
||||
content.addEventListener('basic', cbk, true);
|
||||
content.dispatchEvent(new Event('basic'));
|
||||
testing.expectEqual(2, nb);
|
||||
|
||||
reset()
|
||||
content.removeEventListener('basic', cbk);
|
||||
content.dispatchEvent(new Event('basic'));
|
||||
testing.expectEqual(1, nb);
|
||||
|
||||
reset();
|
||||
content.removeEventListener('basic', cbk, {capture: true});
|
||||
content.dispatchEvent(new Event('basic'));
|
||||
testing.expectEqual(0, nb);
|
||||
|
||||
reset();
|
||||
content.addEventListener('capture', cbk, true);
|
||||
content.dispatchEvent(new Event('capture'));
|
||||
testing.expectEqual(1, nb);
|
||||
testing.expectEqual(true, evt instanceof Event);
|
||||
testing.expectEqual('capture', evt.type);
|
||||
testing.expectEqual(2, phase);
|
||||
testing.expectEqual('content', cur.getAttribute('id'));
|
||||
|
||||
reset();
|
||||
para.dispatchEvent(new Event('capture'));
|
||||
testing.expectEqual(1, nb);
|
||||
testing.expectEqual(true, evt instanceof Event);
|
||||
testing.expectEqual('capture', evt.type);
|
||||
testing.expectEqual(1, phase);
|
||||
testing.expectEqual('content', cur.getAttribute('id'));
|
||||
|
||||
reset();
|
||||
content.addEventListener('bubbles', cbk);
|
||||
content.dispatchEvent(new Event('bubbles', {bubbles: true}));
|
||||
testing.expectEqual(1, nb);
|
||||
testing.expectEqual(true, evt instanceof Event);
|
||||
testing.expectEqual('bubbles', evt.type);
|
||||
testing.expectEqual(2, phase);
|
||||
testing.expectEqual('content', cur.getAttribute('id'));
|
||||
|
||||
reset();
|
||||
para.dispatchEvent(new Event('bubbles', {bubbles: true}));
|
||||
testing.expectEqual(1, nb);
|
||||
testing.expectEqual(true, evt instanceof Event);
|
||||
testing.expectEqual('bubbles', evt.type);
|
||||
testing.expectEqual(3, phase);
|
||||
testing.expectEqual('content', cur.getAttribute('id'));
|
||||
|
||||
|
||||
const obj1 = {
|
||||
calls: 0,
|
||||
handleEvent: function() { this.calls += 1 }
|
||||
};
|
||||
content.addEventListener('he', obj1);
|
||||
content.dispatchEvent(new Event('he'));
|
||||
testing.expectEqual(1, obj1.calls);
|
||||
|
||||
content.removeEventListener('he', obj1);
|
||||
content.dispatchEvent(new Event('he'));
|
||||
testing.expectEqual(1, obj1.calls);
|
||||
|
||||
// doesn't crash on null receiver
|
||||
content.addEventListener('he2', null);
|
||||
content.dispatchEvent(new Event('he2'));
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -20,7 +20,8 @@ const std = @import("std");
|
||||
const js = @import("../js/js.zig");
|
||||
|
||||
const Page = @import("../Page.zig");
|
||||
const RegisterOptions = @import("../EventManager.zig").RegisterOptions;
|
||||
const EventManager = @import("../EventManager.zig");
|
||||
const RegisterOptions = EventManager.RegisterOptions;
|
||||
|
||||
const Event = @import("Event.zig");
|
||||
|
||||
@@ -30,6 +31,7 @@ const _prototype_root = true;
|
||||
_type: Type,
|
||||
|
||||
pub const Type = union(enum) {
|
||||
generic: void,
|
||||
node: *@import("Node.zig"),
|
||||
window: *@import("Window.zig"),
|
||||
xhr: *@import("net/XMLHttpRequestEventTarget.zig"),
|
||||
@@ -42,6 +44,12 @@ pub const Type = union(enum) {
|
||||
screen_orientation: *@import("Screen.zig").Orientation,
|
||||
};
|
||||
|
||||
pub fn init(page: *Page) !*EventTarget {
|
||||
return page._factory.create(EventTarget{
|
||||
._type = .generic,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {
|
||||
try page._event_manager.dispatch(self, event);
|
||||
return !event._cancelable or !event._prevent_default;
|
||||
@@ -59,9 +67,15 @@ pub const EventListenerCallback = union(enum) {
|
||||
pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?AddEventListenerOptions, page: *Page) !void {
|
||||
const callback = callback_ orelse return;
|
||||
|
||||
const actual_callback = switch (callback) {
|
||||
.function => |func| func,
|
||||
.object => |obj| (try obj.getFunction("handleEvent")) orelse return,
|
||||
if (callback == .object) {
|
||||
if (try callback.object.getFunction("handleEvent") == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const em_callback = switch (callback) {
|
||||
.function => |func| EventManager.Callback{ .function = func },
|
||||
.object => |obj| EventManager.Callback{ .object = try obj.persist() },
|
||||
};
|
||||
|
||||
const options = blk: {
|
||||
@@ -71,7 +85,7 @@ pub fn addEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventLi
|
||||
.capture => |capture| RegisterOptions{ .capture = capture },
|
||||
};
|
||||
};
|
||||
return page._event_manager.register(self, typ, actual_callback, options);
|
||||
return page._event_manager.register(self, typ, em_callback, options);
|
||||
}
|
||||
|
||||
const RemoveEventListenerOptions = union(enum) {
|
||||
@@ -79,36 +93,63 @@ const RemoveEventListenerOptions = union(enum) {
|
||||
options: Options,
|
||||
|
||||
const Options = struct {
|
||||
useCapture: bool = false,
|
||||
capture: bool = false,
|
||||
};
|
||||
};
|
||||
pub fn removeEventListener(self: *EventTarget, typ: []const u8, callback_: ?EventListenerCallback, opts_: ?RemoveEventListenerOptions, page: *Page) !void {
|
||||
const callback = callback_ orelse return;
|
||||
|
||||
const actual_callback = switch (callback) {
|
||||
.function => |func| func,
|
||||
.object => |obj| (try obj.getFunction("handleEvent")) orelse return,
|
||||
// For object callbacks, check if handleEvent exists
|
||||
if (callback == .object) {
|
||||
if (try callback.object.getFunction("handleEvent") == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const em_callback = switch (callback) {
|
||||
.function => |func| EventManager.Callback{ .function = func },
|
||||
.object => |obj| EventManager.Callback{ .object = try obj.persist() },
|
||||
};
|
||||
|
||||
const use_capture = blk: {
|
||||
const o = opts_ orelse break :blk false;
|
||||
break :blk switch (o) {
|
||||
.capture => |capture| capture,
|
||||
.options => |opts| opts.useCapture,
|
||||
.options => |opts| opts.capture,
|
||||
};
|
||||
};
|
||||
return page._event_manager.remove(self, typ, actual_callback, use_capture);
|
||||
return page._event_manager.remove(self, typ, em_callback, use_capture);
|
||||
}
|
||||
|
||||
pub fn format(self: *EventTarget, writer: *std.Io.Writer) !void {
|
||||
return switch (self._type) {
|
||||
.node => |n| n.format(writer),
|
||||
.window => writer.writeAll("<window>"),
|
||||
.generic => writer.writeAll("<EventTarget>"),
|
||||
.window => writer.writeAll("<Window>"),
|
||||
.xhr => writer.writeAll("<XMLHttpRequestEventTarget>"),
|
||||
.abort_signal => writer.writeAll("<abort_signal>"),
|
||||
.abort_signal => writer.writeAll("<AbortSignal>"),
|
||||
.media_query_list => writer.writeAll("<MediaQueryList>"),
|
||||
.message_port => writer.writeAll("<MessagePort>"),
|
||||
.text_track_cue => writer.writeAll("<TextTrackCue>"),
|
||||
.navigation => writer.writeAll("<Navigation>"),
|
||||
.screen => writer.writeAll("<Screen>"),
|
||||
.screen_orientation => writer.writeAll("<ScreenOrientation>"),
|
||||
};
|
||||
}
|
||||
|
||||
pub fn toString(self: *EventTarget) []const u8 {
|
||||
return switch (self._type) {
|
||||
.node => |n| return n.className(),
|
||||
.generic => return "[object EventTarget]",
|
||||
.window => return "[object Window]",
|
||||
.xhr => return "[object XMLHttpRequestEventTarget]",
|
||||
.abort_signal => return "[object AbortSignal]",
|
||||
.media_query_list => return "[object MediaQueryList]",
|
||||
.message_port => return "[object MessagePort]",
|
||||
.text_track_cue => return "[object TextTrackCue]",
|
||||
.navigation => return "[object Navigation]",
|
||||
.screen => return "[object Screen]",
|
||||
.screen_orientation => return "[object ScreenOrientation]",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -122,15 +163,16 @@ pub const JsApi = struct {
|
||||
pub var class_id: bridge.ClassId = undefined;
|
||||
};
|
||||
|
||||
pub const constructor = bridge.constructor(EventTarget.init, .{});
|
||||
pub const dispatchEvent = bridge.function(EventTarget.dispatchEvent, .{});
|
||||
pub const addEventListener = bridge.function(EventTarget.addEventListener, .{});
|
||||
pub const removeEventListener = bridge.function(EventTarget.removeEventListener, .{});
|
||||
pub const toString = bridge.function(EventTarget.toString, .{});
|
||||
};
|
||||
|
||||
const testing = @import("../../testing.zig");
|
||||
test "WebApi: EventTarget" {
|
||||
// we create thousands of these per page. Nothing should bloat it.
|
||||
try testing.expectEqual(16, @sizeOf(EventTarget));
|
||||
|
||||
try testing.htmlRunner("events.html", .{});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user