@@ -22,6 +22,7 @@ const Allocator = std.mem.Allocator;
2222const log = @import ("../../log.zig" );
2323const parser = @import ("../netsurf.zig" );
2424const Page = @import ("../page.zig" ).Page ;
25+ const Loop = @import ("../../runtime/loop.zig" ).Loop ;
2526
2627const Env = @import ("../env.zig" ).Env ;
2728const NodeList = @import ("nodelist.zig" ).NodeList ;
@@ -35,25 +36,37 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
3536
3637// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
3738pub const MutationObserver = struct {
39+ loop : * Loop ,
3840 cbk : Env.Function ,
3941 arena : Allocator ,
42+ connected : bool ,
43+ scheduled : bool ,
44+ loop_node : Loop.CallbackNode ,
4045
4146 // List of records which were observed. When the call scope ends, we need to
4247 // execute our callback with it.
43- observed : std .ArrayListUnmanaged (* MutationRecord ),
48+ observed : std .ArrayListUnmanaged (MutationRecord ),
4449
4550 pub fn constructor (cbk : Env.Function , page : * Page ) ! MutationObserver {
4651 return .{
4752 .cbk = cbk ,
53+ .loop = page .loop ,
4854 .observed = .{},
55+ .connected = true ,
56+ .scheduled = false ,
4957 .arena = page .arena ,
58+ .loop_node = .{.func = callback },
5059 };
5160 }
5261
53- pub fn _observe (self : * MutationObserver , node : * parser.Node , options_ : ? MutationObserverInit ) ! void {
54- const options = options_ orelse MutationObserverInit {};
62+ pub fn _observe (self : * MutationObserver , node : * parser.Node , options_ : ? Options ) ! void {
63+ const arena = self .arena ;
64+ var options = options_ orelse Options {};
65+ if (options .attributeFilter .len > 0 ) {
66+ options .attributeFilter = try arena .dupe ([]const u8 , options .attributeFilter );
67+ }
5568
56- const observer = try self . arena .create (Observer );
69+ const observer = try arena .create (Observer );
5770 observer .* = .{
5871 .node = node ,
5972 .options = options ,
@@ -102,30 +115,34 @@ pub const MutationObserver = struct {
102115 }
103116 }
104117
105- pub fn jsCallScopeEnd (self : * MutationObserver ) void {
106- const record = self .observed .items ;
107- if (record .len == 0 ) {
118+ fn callback (node : * Loop.CallbackNode , _ : * ? u63 ) void {
119+ const self : * MutationObserver = @fieldParentPtr ("loop_node" , node );
120+ if (self .connected == false ) {
121+ self .scheduled = true ;
122+ return ;
123+ }
124+ self .scheduled = false ;
125+
126+ const records = self .observed .items ;
127+ if (records .len == 0 ) {
108128 return ;
109129 }
110130
111131 defer self .observed .clearRetainingCapacity ();
112132
113- for (record ) | r | {
114- const records = [_ ]MutationRecord {r .* };
115- var result : Env.Function.Result = undefined ;
116- self .cbk .tryCall (void , .{records }, & result ) catch {
117- log .debug (.user_script , "callback error" , .{
118- .err = result .exception ,
119- .stack = result .stack ,
120- .source = "mutation observer" ,
121- });
122- };
123- }
133+ var result : Env.Function.Result = undefined ;
134+ self .cbk .tryCall (void , .{records }, & result ) catch {
135+ log .debug (.user_script , "callback error" , .{
136+ .err = result .exception ,
137+ .stack = result .stack ,
138+ .source = "mutation observer" ,
139+ });
140+ };
124141 }
125142
126143 // TODO
127- pub fn _disconnect (_ : * MutationObserver ) ! void {
128- // TODO unregister listeners.
144+ pub fn _disconnect (self : * MutationObserver ) ! void {
145+ self . connected = false ;
129146 }
130147
131148 // TODO
@@ -182,59 +199,65 @@ pub const MutationRecord = struct {
182199 }
183200};
184201
185- const MutationObserverInit = struct {
202+ const Options = struct {
186203 childList : bool = false ,
187204 attributes : bool = false ,
188205 characterData : bool = false ,
189206 subtree : bool = false ,
190207 attributeOldValue : bool = false ,
191208 characterDataOldValue : bool = false ,
192- // TODO
193- // attributeFilter: [][]const u8,
209+ attributeFilter : [][]const u8 = &.{},
194210
195- fn attr (self : MutationObserverInit ) bool {
196- return self .attributes or self .attributeOldValue ;
211+ fn attr (self : Options ) bool {
212+ return self .attributes or self .attributeOldValue or self . attributeFilter . len > 0 ;
197213 }
198214
199- fn cdata (self : MutationObserverInit ) bool {
215+ fn cdata (self : Options ) bool {
200216 return self .characterData or self .characterDataOldValue ;
201217 }
202218};
203219
204220const Observer = struct {
205221 node : * parser.Node ,
206- options : MutationObserverInit ,
207-
208- // record of the mutation, all observed changes in 1 call are batched
209- record : ? MutationRecord = null ,
222+ options : Options ,
210223
211224 // reference back to the MutationObserver so that we can access the arena
212225 // and batch the mutation records.
213226 mutation_observer : * MutationObserver ,
214227
215228 event_node : parser.EventNode ,
216229
217- fn appliesTo (o : * const Observer , target : * parser.Node ) bool {
230+ fn appliesTo (self : * const Observer , target : * parser.Node , event_type : MutationEventType , event : * parser.MutationEvent ,) ! bool {
231+ if (event_type == .DOMAttrModified and self .options .attributeFilter .len > 0 ) {
232+ const attribute_name = try parser .mutationEventAttributeName (event );
233+ for (self .options .attributeFilter ) | needle | blk : {
234+ if (std .mem .eql (u8 , attribute_name , needle )) {
235+ break :blk ;
236+ }
237+ }
238+ return false ;
239+ }
240+
218241 // mutation on any target is always ok.
219- if (o .options .subtree ) {
242+ if (self .options .subtree ) {
220243 return true ;
221244 }
222245
223246 // if target equals node, alway ok.
224- if (target == o .node ) {
247+ if (target == self .node ) {
225248 return true ;
226249 }
227250
228251 // no subtree, no same target and no childlist, always noky.
229- if (! o .options .childList ) {
252+ if (! self .options .childList ) {
230253 return false ;
231254 }
232255
233256 // target must be a child of o.node
234257 const walker = Walker {};
235258 var next : ? * parser.Node = null ;
236259 while (true ) {
237- next = walker .get_next (o .node , next ) catch break orelse break ;
260+ next = walker .get_next (self .node , next ) catch break orelse break ;
238261 if (next .? == target ) {
239262 return true ;
240263 }
@@ -258,27 +281,22 @@ const Observer = struct {
258281 break :blk parser .eventTargetToNode (event_target );
259282 };
260283
261- if (self .appliesTo (node ) == false ) {
262- return ;
263- }
264-
284+ const mutation_event = parser .eventToMutationEvent (event );
265285 const event_type = blk : {
266286 const t = try parser .eventType (event );
267287 break :blk std .meta .stringToEnum (MutationEventType , t ) orelse return ;
268288 };
269289
270- const arena = mutation_observer .arena ;
271- if (self .record == null ) {
272- self .record = .{
273- .target = self .node ,
274- .type = event_type .recordType (),
275- };
276- try mutation_observer .observed .append (arena , & self .record .? );
290+ if (try self .appliesTo (node , event_type , mutation_event ) == false ) {
291+ return ;
277292 }
278293
279- var record = & self .record .? ;
280- const mutation_event = parser .eventToMutationEvent (event );
294+ var record = MutationRecord {
295+ .target = self .node ,
296+ .type = event_type .recordType (),
297+ };
281298
299+ const arena = mutation_observer .arena ;
282300 switch (event_type ) {
283301 .DOMAttrModified = > {
284302 record .attribute_name = parser .mutationEventAttributeName (mutation_event ) catch null ;
@@ -302,6 +320,13 @@ const Observer = struct {
302320 }
303321 },
304322 }
323+
324+ try mutation_observer .observed .append (arena , record );
325+
326+ if (mutation_observer .scheduled == false ) {
327+ mutation_observer .scheduled = true ;
328+ _ = try mutation_observer .loop .timeout (0 , & mutation_observer .loop_node );
329+ }
305330 }
306331};
307332
@@ -341,10 +366,9 @@ test "Browser.DOM.MutationObserver" {
341366 \\ document.firstElementChild.setAttribute("foo", "bar");
342367 \\ // ignored b/c it's about another target.
343368 \\ document.firstElementChild.firstChild.setAttribute("foo", "bar");
344- \\ nb;
345- ,
346- "1" ,
369+ ,null
347370 },
371+ .{ "nb" , "1" },
348372 .{ "mrs[0].type" , "attributes" },
349373 .{ "mrs[0].target == document.firstElementChild" , "true" },
350374 .{ "mrs[0].target.getAttribute('foo')" , "bar" },
@@ -362,10 +386,9 @@ test "Browser.DOM.MutationObserver" {
362386 \\ nb2++;
363387 \\ }).observe(node, { characterData: true, characterDataOldValue: true });
364388 \\ node.data = "foo";
365- \\ nb2;
366- ,
367- "1" ,
389+ , null
368390 },
391+ .{ "nb2" , "1" },
369392 .{ "mrs2[0].type" , "characterData" },
370393 .{ "mrs2[0].target == node" , "true" },
371394 .{ "mrs2[0].target.data" , "foo" },
@@ -382,8 +405,23 @@ test "Browser.DOM.MutationObserver" {
382405 \\ node.innerText = 'a';
383406 \\ }).observe(document, { subtree:true,childList:true });
384407 \\ node.innerText = "2";
385- ,
386- "2" ,
408+ , null
409+ },
410+ .{"node.innerText" , "a" },
411+ }, .{});
412+
413+ try runner .testCases (&.{
414+ .{
415+ \\ var node = document.getElementById("para");
416+ \\ var attrWatch = 0;
417+ \\ new MutationObserver(() => {
418+ \\ attrWatch++;
419+ \\ }).observe(document, { attributeFilter: ["name"], subtree: true });
420+ \\ node.setAttribute("id", "1");
421+ , null
387422 },
423+ .{"attrWatch" , "0" },
424+ .{ "node.setAttribute('name', 'other');" , null },
425+ .{ "attrWatch" , "1" },
388426 }, .{});
389427}
0 commit comments