@@ -22,6 +22,7 @@ const parser = @import("netsurf");
2222
2323const jsruntime = @import ("jsruntime" );
2424const Callback = jsruntime .Callback ;
25+ const CallbackResult = jsruntime .CallbackResult ;
2526const Case = jsruntime .test_utils .Case ;
2627const checkCases = jsruntime .test_utils .checkCases ;
2728
@@ -31,14 +32,35 @@ const NodeList = @import("nodelist.zig").NodeList;
3132
3233pub const Interfaces = generate .Tuple (.{
3334 MutationObserver ,
35+ MutationRecord ,
36+ MutationRecords ,
3437});
3538
39+ const Walker = @import ("../dom/walker.zig" ).WalkerChildren ;
40+
41+ const log = std .log .scoped (.events );
42+
3643// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
3744pub const MutationObserver = struct {
3845 cbk : Callback ,
46+ observers : Observers ,
3947
4048 pub const mem_guarantied = true ;
4149
50+ const Observer = struct {
51+ node : * parser.Node ,
52+ options : MutationObserverInit ,
53+ };
54+
55+ const deinitFunc = struct {
56+ fn deinit (ctx : ? * anyopaque , alloc : std.mem.Allocator ) void {
57+ const o : * Observer = @ptrCast (@alignCast (ctx ));
58+ alloc .destroy (o );
59+ }
60+ }.deinit ;
61+
62+ const Observers = std .ArrayListUnmanaged (* Observer );
63+
4264 pub const MutationObserverInit = struct {
4365 childList : bool = false ,
4466 attributes : bool = false ,
@@ -48,24 +70,260 @@ pub const MutationObserver = struct {
4870 characterDataOldValue : bool = false ,
4971 // TODO
5072 // attributeFilter: [][]const u8,
73+
74+ fn attr (self : MutationObserverInit ) bool {
75+ return self .attributes or self .attributeOldValue ;
76+ }
77+
78+ fn cdata (self : MutationObserverInit ) bool {
79+ return self .characterData or self .characterDataOldValue ;
80+ }
5181 };
5282
5383 pub fn constructor (cbk : Callback ) ! MutationObserver {
5484 return MutationObserver {
5585 .cbk = cbk ,
86+ .observers = .{},
5687 };
5788 }
5889
59- pub fn _observe (
60- _ : * MutationObserver ,
61- _ : * parser.Node ,
62- _ : ? MutationObserverInit ,
63- ) ! void {}
90+ // TODO
91+ fn resolveOptions (opt : ? MutationObserverInit ) MutationObserverInit {
92+ return opt orelse .{};
93+ }
94+
95+ pub fn _observe (self : * MutationObserver , alloc : std.mem.Allocator , node : * parser.Node , options : ? MutationObserverInit ) ! void {
96+ const o = try alloc .create (Observer );
97+ o .* = .{
98+ .node = node ,
99+ .options = resolveOptions (options ),
100+ };
101+ errdefer alloc .destroy (o );
102+
103+ // register the new observer.
104+ try self .observers .append (alloc , o );
105+
106+ // register node's events.
107+ if (o .options .childList or o .options .subtree ) {
108+ try parser .eventTargetAddEventListener (
109+ parser .toEventTarget (parser .Node , node ),
110+ alloc ,
111+ "DOMNodeInserted" ,
112+ EventHandler ,
113+ .{ .cbk = self .cbk , .data = o , .deinitFunc = deinitFunc },
114+ false ,
115+ );
116+ try parser .eventTargetAddEventListener (
117+ parser .toEventTarget (parser .Node , node ),
118+ alloc ,
119+ "DOMNodeRemoved" ,
120+ EventHandler ,
121+ .{ .cbk = self .cbk , .data = o , .deinitFunc = deinitFunc },
122+ false ,
123+ );
124+ }
125+ if (o .options .attr ()) {
126+ try parser .eventTargetAddEventListener (
127+ parser .toEventTarget (parser .Node , node ),
128+ alloc ,
129+ "DOMAttrModified" ,
130+ EventHandler ,
131+ .{ .cbk = self .cbk , .data = o , .deinitFunc = deinitFunc },
132+ false ,
133+ );
134+ }
135+ if (o .options .cdata ()) {
136+ try parser .eventTargetAddEventListener (
137+ parser .toEventTarget (parser .Node , node ),
138+ alloc ,
139+ "DOMCharacterDataModified" ,
140+ EventHandler ,
141+ .{ .cbk = self .cbk , .data = o , .deinitFunc = deinitFunc },
142+ false ,
143+ );
144+ }
145+ if (o .options .subtree ) {
146+ try parser .eventTargetAddEventListener (
147+ parser .toEventTarget (parser .Node , node ),
148+ alloc ,
149+ "DOMSubtreeModified" ,
150+ EventHandler ,
151+ .{ .cbk = self .cbk , .data = o , .deinitFunc = deinitFunc },
152+ false ,
153+ );
154+ }
155+ }
156+
157+ // TODO
158+ pub fn _disconnect (_ : * MutationObserver ) ! void {
159+ // TODO unregister listeners.
160+ }
161+
162+ pub fn deinit (self : * MutationObserver , alloc : std.mem.Allocator ) void {
163+ // TODO unregister listeners.
164+ for (self .observers .items ) | o | alloc .destroy (o );
165+ self .observers .deinit (alloc );
166+ }
64167
65168 // TODO
66- pub fn _disconnect (_ : * MutationObserver ) ! void {}
169+ pub fn _takeRecords (_ : MutationObserver ) ? []const u8 {
170+ return &[_ ]u8 {};
171+ }
67172};
68173
174+ // Handle multiple record?
175+ pub const MutationRecords = struct {
176+ first : ? MutationRecord = null ,
177+
178+ pub const mem_guarantied = true ;
179+
180+ pub fn get_length (self : * MutationRecords ) u32 {
181+ if (self .first == null ) return 0 ;
182+
183+ return 1 ;
184+ }
185+
186+ pub fn postAttach (self : * MutationRecords , js_obj : jsruntime.JSObject ) ! void {
187+ if (self .first ) | mr | {
188+ try js_obj .set ("0" , mr );
189+ }
190+ }
191+ };
192+
193+ pub const MutationRecord = struct {
194+ type : []const u8 ,
195+ target : * parser.Node ,
196+ addedNodes : NodeList = NodeList .init (),
197+ removedNodes : NodeList = NodeList .init (),
198+ previousSibling : ? * parser.Node = null ,
199+ nextSibling : ? * parser.Node = null ,
200+ attributeName : ? []const u8 = null ,
201+ attributeNamespace : ? []const u8 = null ,
202+ oldValue : ? []const u8 = null ,
203+
204+ pub const mem_guarantied = true ;
205+
206+ pub fn get_type (self : MutationRecord ) []const u8 {
207+ return self .type ;
208+ }
209+
210+ pub fn get_addedNodes (self : MutationRecord ) NodeList {
211+ return self .addedNodes ;
212+ }
213+
214+ pub fn get_removedNodes (self : MutationRecord ) NodeList {
215+ return self .addedNodes ;
216+ }
217+
218+ pub fn get_target (self : MutationRecord ) * parser.Node {
219+ return self .target ;
220+ }
221+
222+ pub fn get_attributeName (self : MutationRecord ) ? []const u8 {
223+ return self .attributeName ;
224+ }
225+
226+ pub fn get_attributeNamespace (self : MutationRecord ) ? []const u8 {
227+ return self .attributeNamespace ;
228+ }
229+
230+ pub fn get_previousSibling (self : MutationRecord ) ? * parser.Node {
231+ return self .previousSibling ;
232+ }
233+
234+ pub fn get_nextSibling (self : MutationRecord ) ? * parser.Node {
235+ return self .nextSibling ;
236+ }
237+
238+ pub fn get_oldValue (self : MutationRecord ) ? []const u8 {
239+ return self .oldValue ;
240+ }
241+ };
242+
243+ // EventHandler dedicated to mutation events.
244+ const EventHandler = struct {
245+ fn apply (o : * MutationObserver.Observer , target : * parser.Node ) bool {
246+ // mutation on any target is always ok.
247+ if (o .options .subtree ) return true ;
248+ // if target equals node, alway ok.
249+ if (target == o .node ) return true ;
250+
251+ // no subtree, no same target and no childlist, always noky.
252+ if (! o .options .childList ) return false ;
253+
254+ // target must be a child of o.node
255+ const walker = Walker {};
256+ var next : ? * parser.Node = null ;
257+ while (true ) {
258+ next = walker .get_next (o .node , next ) catch break orelse break ;
259+ if (next .? == target ) return true ;
260+ }
261+
262+ return false ;
263+ }
264+
265+ fn handle (evt : ? * parser.Event , data : parser.EventHandlerData ) void {
266+ if (evt == null ) return ;
267+
268+ var mrs : MutationRecords = .{};
269+
270+ const t = parser .eventType (evt .? ) catch | e | {
271+ log .err ("mutation observer event type: {any}" , .{e });
272+ return ;
273+ };
274+ const et = parser .eventTarget (evt .? ) catch | e | {
275+ log .err ("mutation observer event target: {any}" , .{e });
276+ return ;
277+ } orelse return ;
278+ const node = parser .eventTargetToNode (et );
279+
280+ // retrieve the observer from the data.
281+ const o : * MutationObserver.Observer = @ptrCast (@alignCast (data .data ));
282+
283+ if (! apply (o , node )) return ;
284+
285+ const muevt = parser .eventToMutationEvent (evt .? );
286+
287+ if (std .mem .eql (u8 , t , "DOMAttrModified" )) {
288+ mrs .first = .{
289+ .type = "attributes" ,
290+ .target = o .node ,
291+ .attributeName = parser .mutationEventAttributeName (muevt ) catch null ,
292+ };
293+
294+ // record old value if required.
295+ if (o .options .attributeOldValue ) {
296+ mrs .first .? .oldValue = parser .mutationEventPrevValue (muevt ) catch null ;
297+ }
298+ } else if (std .mem .eql (u8 , t , "DOMCharacterDataModified" )) {
299+ mrs .first = .{
300+ .type = "characterData" ,
301+ .target = o .node ,
302+ };
303+
304+ // record old value if required.
305+ if (o .options .characterDataOldValue ) {
306+ mrs .first .? .oldValue = parser .mutationEventPrevValue (muevt ) catch null ;
307+ }
308+ } else {
309+ return ;
310+ }
311+
312+ // TODO get the allocator by another way?
313+ var res = CallbackResult .init (data .cbk .nat_ctx .alloc );
314+ defer res .deinit ();
315+
316+ // TODO pass MutationRecords and MutationObserver
317+ data .cbk .trycall (.{mrs }, & res ) catch | e | log .err ("mutation event handler error: {any}" , .{e });
318+
319+ // in case of function error, we log the result and the trace.
320+ if (! res .success ) {
321+ log .info ("mutation observer event handler error: {s}" , .{res .result orelse "unknown" });
322+ log .debug ("{s}" , .{res .stack orelse "no stack trace" });
323+ }
324+ }
325+ }.handle ;
326+
69327pub fn testExecFn (
70328 _ : std.mem.Allocator ,
71329 js_env : * jsruntime.Env ,
@@ -74,4 +332,44 @@ pub fn testExecFn(
74332 .{ .src = "new MutationObserver(() => {}).observe(document, { childList: true });" , .ex = "undefined" },
75333 };
76334 try checkCases (js_env , & constructor );
335+
336+ var attr = [_ ]Case {
337+ .{ .src =
338+ \\var nb = 0;
339+ \\var mrs;
340+ \\new MutationObserver((mu) => {
341+ \\ mrs = mu;
342+ \\ nb++;
343+ \\}).observe(document.firstElementChild, { attributes: true, attributeOldValue: true });
344+ \\document.firstElementChild.setAttribute("foo", "bar");
345+ \\// ignored b/c it's about another target.
346+ \\document.firstElementChild.firstChild.setAttribute("foo", "bar");
347+ \\nb;
348+ , .ex = "1" },
349+ .{ .src = "mrs[0].type" , .ex = "attributes" },
350+ .{ .src = "mrs[0].target == document.firstElementChild" , .ex = "true" },
351+ .{ .src = "mrs[0].target.getAttribute('foo')" , .ex = "bar" },
352+ .{ .src = "mrs[0].attributeName" , .ex = "foo" },
353+ .{ .src = "mrs[0].oldValue" , .ex = "null" },
354+ };
355+ try checkCases (js_env , & attr );
356+
357+ var cdata = [_ ]Case {
358+ .{ .src =
359+ \\var node = document.getElementById("para").firstChild;
360+ \\var nb2 = 0;
361+ \\var mrs2;
362+ \\new MutationObserver((mu) => {
363+ \\ mrs2 = mu;
364+ \\ nb2++;
365+ \\}).observe(node, { characterData: true, characterDataOldValue: true });
366+ \\node.data = "foo";
367+ \\nb2;
368+ , .ex = "1" },
369+ .{ .src = "mrs2[0].type" , .ex = "characterData" },
370+ .{ .src = "mrs2[0].target == node" , .ex = "true" },
371+ .{ .src = "mrs2[0].target.data" , .ex = "foo" },
372+ .{ .src = "mrs2[0].oldValue" , .ex = " And" },
373+ };
374+ try checkCases (js_env , & cdata );
77375}
0 commit comments