diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig
index 42de7d55..53e2c6ad 100644
--- a/src/browser/EventManager.zig
+++ b/src/browser/EventManager.zig
@@ -61,6 +61,7 @@ arena: Allocator,
// 'load' event (e.g. amazon product page has no listener and ~350 resources)
has_dom_load_listener: bool,
listener_pool: std.heap.MemoryPool(Listener),
+ignore_list: std.ArrayList(*Listener),
list_pool: std.heap.MemoryPool(std.DoublyLinkedList),
lookup: std.HashMapUnmanaged(
EventKey,
@@ -76,6 +77,7 @@ pub fn init(arena: Allocator, page: *Page) EventManager {
.page = page,
.lookup = .{},
.arena = arena,
+ .ignore_list = .{},
.list_pool = .init(arena),
.listener_pool = .init(arena),
.dispatch_depth = 0,
@@ -155,6 +157,11 @@ pub fn register(self: *EventManager, target: *EventTarget, typ: []const u8, call
};
// append the listener to the list of listeners for this target
gop.value_ptr.*.append(&listener.node);
+
+ // Track load listeners for script execution ignore list
+ if (type_string.eql(comptime .wrap("load"))) {
+ try self.ignore_list.append(self.arena, listener);
+ }
}
pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callback: Callback, use_capture: bool) void {
@@ -167,6 +174,10 @@ pub fn remove(self: *EventManager, target: *EventTarget, typ: []const u8, callba
}
}
+pub fn clearIgnoreList(self: *EventManager) void {
+ self.ignore_list.clearRetainingCapacity();
+}
+
// Dispatching can be recursive from the compiler's point of view, so we need to
// give it an explicit error set so that other parts of the code can use and
// inferred error.
@@ -178,7 +189,21 @@ const DispatchError = error{
ExecutionError,
JsException,
};
+
+pub const DispatchOpts = struct {
+ // A "load" event triggered by a script (in ScriptManager) should not trigger
+ // a "load" listener added within that script. Therefore, any "load" listener
+ // that we add go into an ignore list until after the script finishes executing.
+ // The ignore list is only checked when apply_ignore == true, which is only
+ // set by the ScriptManager when raising the script's "load" event.
+ apply_ignore: bool = false,
+};
+
pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) DispatchError!void {
+ return self.dispatchOpts(target, event, .{});
+}
+
+pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
defer if (!event._v8_handoff) event.deinit(false, self.page);
if (comptime IS_DEBUG) {
@@ -197,7 +222,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
};
switch (target._type) {
- .node => |node| try self.dispatchNode(node, event, &was_handled),
+ .node => |node| try self.dispatchNode(node, event, &was_handled, opts),
.xhr,
.window,
.abort_signal,
@@ -214,7 +239,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
.event_target = @intFromPtr(target),
.type_string = event._type_string,
}) orelse return;
- try self.dispatchAll(list, target, event, &was_handled);
+ try self.dispatchAll(list, target, event, &was_handled, opts);
},
}
}
@@ -262,10 +287,10 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
.event_target = @intFromPtr(target),
.type_string = event._type_string,
}) orelse return;
- try self.dispatchAll(list, target, event, &was_dispatched);
+ try self.dispatchAll(list, target, event, &was_dispatched, .{});
}
-fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
+fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool, comptime opts: DispatchOpts) !void {
const ShadowRoot = @import("webapi/ShadowRoot.zig");
const page = self.page;
@@ -346,7 +371,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
.event_target = @intFromPtr(current_target),
.type_string = event._type_string,
})) |list| {
- try self.dispatchPhase(list, current_target, event, was_handled, true);
+ try self.dispatchPhase(list, current_target, event, was_handled, comptime .init(true, opts));
}
}
@@ -380,7 +405,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
.type_string = event._type_string,
.event_target = @intFromPtr(target_et),
})) |list| {
- try self.dispatchPhase(list, target_et, event, was_handled, null);
+ try self.dispatchPhase(list, target_et, event, was_handled, comptime .init(null, opts));
if (event._stop_propagation) {
return;
}
@@ -397,13 +422,25 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
.type_string = event._type_string,
.event_target = @intFromPtr(current_target),
})) |list| {
- try self.dispatchPhase(list, current_target, event, was_handled, false);
+ try self.dispatchPhase(list, current_target, event, was_handled, comptime .init(false, opts));
}
}
}
}
-fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void {
+const DispatchPhaseOpts = struct {
+ capture_only: ?bool = null,
+ apply_ignore: bool = false,
+
+ fn init(capture_only: ?bool, opts: DispatchOpts) DispatchPhaseOpts {
+ return .{
+ .capture_only = capture_only,
+ .apply_ignore = opts.apply_ignore,
+ };
+ }
+};
+
+fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime opts: DispatchPhaseOpts) !void {
const page = self.page;
// Track dispatch depth for deferred removal
@@ -429,7 +466,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
// Iterate through the list, stopping after we've encountered the last_listener
var node = list.first;
var is_done = false;
- while (node) |n| {
+ node_loop: while (node) |n| {
if (is_done) {
break;
}
@@ -439,7 +476,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
node = n.next;
// Skip non-matching listeners
- if (comptime capture_only) |capture| {
+ if (comptime opts.capture_only) |capture| {
if (listener.capture != capture) {
continue;
}
@@ -458,6 +495,14 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
}
}
+ if (comptime opts.apply_ignore) {
+ for (self.ignore_list.items) |ignored| {
+ if (ignored == listener) {
+ continue :node_loop;
+ }
+ }
+ }
+
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
if (listener.once) {
self.removeListener(list, listener);
@@ -502,8 +547,8 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
}
// Non-Node dispatching (XHR, Window without propagation)
-fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool) !void {
- return self.dispatchPhase(list, current_target, event, was_handled, null);
+fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime opts: DispatchOpts) !void {
+ return self.dispatchPhase(list, current_target, event, was_handled, comptime .init(null, opts));
}
fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {
diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig
index f4d95230..b536e2db 100644
--- a/src/browser/ScriptManager.zig
+++ b/src/browser/ScriptManager.zig
@@ -20,13 +20,14 @@ const std = @import("std");
const lp = @import("lightpanda");
const builtin = @import("builtin");
-const js = @import("js/js.zig");
const log = @import("../log.zig");
+const Http = @import("../http/Http.zig");
+const String = @import("../string.zig").String;
+const js = @import("js/js.zig");
const URL = @import("URL.zig");
const Page = @import("Page.zig");
const Browser = @import("Browser.zig");
-const Http = @import("../http/Http.zig");
const Element = @import("webapi/Element.zig");
@@ -830,13 +831,15 @@ pub const Script = struct {
.kind = self.kind,
.cacheable = cacheable,
});
- self.executeCallback("error", local.toLocal(script_element._on_error), page);
+ self.executeCallback(comptime .wrap("error"), page);
return;
};
- self.executeCallback("load", local.toLocal(script_element._on_load), page);
+ self.executeCallback(comptime .wrap("load"), page);
return;
}
+ defer page._event_manager.clearIgnoreList();
+
var try_catch: js.TryCatch = undefined;
try_catch.init(local);
defer try_catch.deinit();
@@ -855,7 +858,7 @@ pub const Script = struct {
};
if (comptime IS_DEBUG) {
- log.debug(.browser, "executed script", .{ .src = url, .success = success, .on_load = script_element._on_load != null });
+ log.debug(.browser, "executed script", .{ .src = url, .success = success });
}
defer {
@@ -867,7 +870,7 @@ pub const Script = struct {
}
if (success) {
- self.executeCallback("load", local.toLocal(script_element._on_load), page);
+ self.executeCallback(comptime .wrap("load"), page);
return;
}
@@ -878,14 +881,12 @@ pub const Script = struct {
.cacheable = cacheable,
});
- self.executeCallback("error", local.toLocal(script_element._on_error), page);
+ self.executeCallback(comptime .wrap("error"), page);
}
- fn executeCallback(self: *const Script, comptime typ: []const u8, cb_: ?js.Function, page: *Page) void {
- const cb = cb_ orelse return;
-
+ fn executeCallback(self: *const Script, typ: String, page: *Page) void {
const Event = @import("webapi/Event.zig");
- const event = Event.initTrusted(comptime .wrap(typ), .{}, page) catch |err| {
+ const event = Event.initTrusted(typ, .{}, page) catch |err| {
log.warn(.js, "script internal callback", .{
.url = self.url,
.type = typ,
@@ -893,14 +894,11 @@ pub const Script = struct {
});
return;
};
- defer if (!event._v8_handoff) event.deinit(false, self.manager.page);
-
- var caught: js.TryCatch.Caught = undefined;
- cb.tryCall(void, .{event}, &caught) catch {
+ page._event_manager.dispatchOpts(self.script_element.?.asNode().asEventTarget(), event, .{ .apply_ignore = true }) catch |err| {
log.warn(.js, "script callback", .{
.url = self.url,
.type = typ,
- .caught = caught,
+ .err = err,
});
};
}
diff --git a/src/browser/tests/element/html/script/empty.js b/src/browser/tests/element/html/script/empty.js
new file mode 100644
index 00000000..e69de29b
diff --git a/src/browser/tests/element/html/script/script.html b/src/browser/tests/element/html/script/script.html
index 3629cf1a..75547d40 100644
--- a/src/browser/tests/element/html/script/script.html
+++ b/src/browser/tests/element/html/script/script.html
@@ -2,11 +2,28 @@
diff --git a/src/browser/webapi/element/html/Script.zig b/src/browser/webapi/element/html/Script.zig
index 92b37199..41899bb4 100644
--- a/src/browser/webapi/element/html/Script.zig
+++ b/src/browser/webapi/element/html/Script.zig
@@ -30,8 +30,6 @@ const Script = @This();
_proto: *HtmlElement,
_src: []const u8 = "",
-_on_load: ?js.Function.Global = null,
-_on_error: ?js.Function.Global = null,
_executed: bool = false,
pub fn asElement(self: *Script) *Element {
@@ -108,22 +106,6 @@ pub fn setDefer(self: *Script, value: bool, page: *Page) !void {
}
}
-pub fn getOnLoad(self: *const Script) ?js.Function.Global {
- return self._on_load;
-}
-
-pub fn setOnLoad(self: *Script, cb: ?js.Function.Global) void {
- self._on_load = cb;
-}
-
-pub fn getOnError(self: *const Script) ?js.Function.Global {
- return self._on_error;
-}
-
-pub fn setOnError(self: *Script, cb: ?js.Function.Global) void {
- self._on_error = cb;
-}
-
pub fn getNoModule(self: *const Script) bool {
return self.asConstElement().getAttributeSafe(comptime .wrap("nomodule")) != null;
}
@@ -147,8 +129,6 @@ pub const JsApi = struct {
pub const @"type" = bridge.accessor(Script.getType, Script.setType, .{});
pub const nonce = bridge.accessor(Script.getNonce, Script.setNonce, .{});
pub const charset = bridge.accessor(Script.getCharset, Script.setCharset, .{});
- pub const onload = bridge.accessor(Script.getOnLoad, Script.setOnLoad, .{});
- pub const onerror = bridge.accessor(Script.getOnError, Script.setOnError, .{});
pub const noModule = bridge.accessor(Script.getNoModule, null, .{});
pub const innerText = bridge.accessor(_innerText, Script.setInnerText, .{});
fn _innerText(self: *Script, page: *const Page) ![]const u8 {
@@ -160,26 +140,10 @@ pub const JsApi = struct {
};
pub const Build = struct {
- pub fn complete(node: *Node, page: *Page) !void {
+ pub fn complete(node: *Node, _: *Page) !void {
const self = node.as(Script);
const element = self.asElement();
self._src = element.getAttributeSafe(comptime .wrap("src")) orelse "";
-
- if (element.getAttributeSafe(comptime .wrap("onload"))) |on_load| {
- if (page.js.stringToPersistedFunction(on_load)) |func| {
- self._on_load = func;
- } else |err| {
- log.err(.js, "script.onload", .{ .err = err, .str = on_load });
- }
- }
-
- if (element.getAttributeSafe(comptime .wrap("onerror"))) |on_error| {
- if (page.js.stringToPersistedFunction(on_error)) |func| {
- self._on_error = func;
- } else |err| {
- log.err(.js, "script.onerror", .{ .err = err, .str = on_error });
- }
- }
}
};