View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host.dom;
16  
17  import static org.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
18  import static org.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
19  
20  import org.htmlunit.corejs.javascript.Function;
21  import org.htmlunit.corejs.javascript.NativeArray;
22  import org.htmlunit.corejs.javascript.NativeObject;
23  import org.htmlunit.corejs.javascript.Scriptable;
24  import org.htmlunit.html.CharacterDataChangeEvent;
25  import org.htmlunit.html.CharacterDataChangeListener;
26  import org.htmlunit.html.HtmlAttributeChangeEvent;
27  import org.htmlunit.html.HtmlAttributeChangeListener;
28  import org.htmlunit.html.HtmlElement;
29  import org.htmlunit.html.HtmlPage;
30  import org.htmlunit.javascript.HtmlUnitScriptable;
31  import org.htmlunit.javascript.JavaScriptEngine;
32  import org.htmlunit.javascript.PostponedAction;
33  import org.htmlunit.javascript.configuration.JsxClass;
34  import org.htmlunit.javascript.configuration.JsxConstructor;
35  import org.htmlunit.javascript.configuration.JsxConstructorAlias;
36  import org.htmlunit.javascript.configuration.JsxFunction;
37  import org.htmlunit.javascript.host.Window;
38  
39  /**
40   * A JavaScript object for {@code MutationObserver}.
41   *
42   * @author Ahmed Ashour
43   * @author Ronald Brill
44   * @author Atsushi Nakagawa
45   */
46  @JsxClass
47  public class MutationObserver extends HtmlUnitScriptable implements HtmlAttributeChangeListener,
48          CharacterDataChangeListener {
49  
50      private Function function_;
51      private Node node_;
52      private boolean attaributes_;
53      private boolean attributeOldValue_;
54      private NativeArray attributeFilter_;
55      private boolean characterData_;
56      private boolean characterDataOldValue_;
57      private boolean subtree_;
58  
59      /**
60       * Creates an instance.
61       * @param function the function to observe
62       */
63      @JsxConstructor
64      @JsxConstructorAlias(value = {CHROME, EDGE}, alias = "WebKitMutationObserver")
65      public void jsConstructor(final Function function) {
66          function_ = function;
67      }
68  
69      /**
70       * Registers the {@link MutationObserver} instance to receive notifications of DOM mutations on the specified node.
71       * @param node the node
72       * @param options the options
73       */
74      @JsxFunction
75      public void observe(final Node node, final NativeObject options) {
76          if (node == null) {
77              throw JavaScriptEngine.typeError("Node is undefined");
78          }
79          if (options == null) {
80              throw JavaScriptEngine.typeError("Options is undefined");
81          }
82  
83          node_ = node;
84          attaributes_ = Boolean.TRUE.equals(options.get("attributes"));
85          attributeOldValue_ = Boolean.TRUE.equals(options.get("attributeOldValue"));
86          characterData_ = Boolean.TRUE.equals(options.get("characterData"));
87          characterDataOldValue_ = Boolean.TRUE.equals(options.get("characterDataOldValue"));
88          subtree_ = Boolean.TRUE.equals(options.get("subtree"));
89          attributeFilter_ = (NativeArray) options.get("attributeFilter");
90  
91          final boolean childList = Boolean.TRUE.equals(options.get("childList"));
92  
93          if (!attaributes_ && !childList && !characterData_) {
94              throw JavaScriptEngine.typeError("One of childList, attributes, od characterData must be set");
95          }
96  
97          if (attaributes_ && node_.getDomNodeOrDie() instanceof HtmlElement) {
98              ((HtmlElement) node_.getDomNodeOrDie()).addHtmlAttributeChangeListener(this);
99          }
100         if (characterData_) {
101             node.getDomNodeOrDie().addCharacterDataChangeListener(this);
102         }
103     }
104 
105     /**
106      * Stops the MutationObserver instance from receiving notifications of DOM mutations.
107      */
108     @JsxFunction
109     public void disconnect() {
110         if (attaributes_ && node_.getDomNodeOrDie() instanceof HtmlElement) {
111             ((HtmlElement) node_.getDomNodeOrDie()).removeHtmlAttributeChangeListener(this);
112         }
113         if (characterData_) {
114             node_.getDomNodeOrDie().removeCharacterDataChangeListener(this);
115         }
116     }
117 
118     /**
119      * Empties the MutationObserver instance's record queue and returns what was in there.
120      * @return an {@link NativeArray} of {@link MutationRecord}s
121      */
122     @JsxFunction
123     public Scriptable takeRecords() {
124         return JavaScriptEngine.newArray(getParentScope(), 0);
125     }
126 
127     /**
128      * {@inheritDoc}
129      */
130     @Override
131     public void characterDataChanged(final CharacterDataChangeEvent event) {
132         final HtmlUnitScriptable target = event.getCharacterData().getScriptableObject();
133         if (subtree_ || target == node_) {
134             final MutationRecord mutationRecord = new MutationRecord();
135             final Scriptable scope = getParentScope();
136             mutationRecord.setParentScope(scope);
137             mutationRecord.setPrototype(getPrototype(mutationRecord.getClass()));
138 
139             mutationRecord.setType("characterData");
140             mutationRecord.setTarget(target);
141             if (characterDataOldValue_) {
142                 mutationRecord.setOldValue(event.getOldValue());
143             }
144 
145             final Window window = getWindow();
146             final HtmlPage owningPage = (HtmlPage) window.getDocument().getPage();
147             final JavaScriptEngine jsEngine =
148                     (JavaScriptEngine) window.getWebWindow().getWebClient().getJavaScriptEngine();
149             jsEngine.addPostponedAction(new PostponedAction(owningPage, "MutationObserver.characterDataChanged") {
150                 @Override
151                 public void execute() {
152                     final Scriptable array = JavaScriptEngine.newArray(scope, new Object[] {mutationRecord});
153                     jsEngine.callFunction(owningPage, function_, scope, MutationObserver.this, new Object[] {array});
154                 }
155             });
156         }
157     }
158 
159     /**
160      * {@inheritDoc}
161      */
162     @Override
163     public void attributeAdded(final HtmlAttributeChangeEvent event) {
164         attributeChanged(event, "MutationObserver.attributeAdded", false);
165     }
166 
167     /**
168      * {@inheritDoc}
169      */
170     @Override
171     public void attributeRemoved(final HtmlAttributeChangeEvent event) {
172         attributeChanged(event, "MutationObserver.attributeRemoved", true);
173     }
174 
175     /**
176      * {@inheritDoc}
177      */
178     @Override
179     public void attributeReplaced(final HtmlAttributeChangeEvent event) {
180         attributeChanged(event, "MutationObserver.attributeReplaced", true);
181     }
182 
183     private void attributeChanged(final HtmlAttributeChangeEvent event, final String actionTitle,
184                         final boolean includeOldValue) {
185         final HtmlElement target = event.getHtmlElement();
186         if (subtree_ || target == node_.getDomNodeOrDie()) {
187             final String attributeName = event.getName();
188             if (attributeFilter_ == null || attributeFilter_.contains(attributeName)) {
189                 final MutationRecord mutationRecord = new MutationRecord();
190                 final Scriptable scope = getParentScope();
191                 mutationRecord.setParentScope(scope);
192                 mutationRecord.setPrototype(getPrototype(mutationRecord.getClass()));
193 
194                 mutationRecord.setAttributeName(attributeName);
195                 mutationRecord.setType("attributes");
196                 mutationRecord.setTarget(target.getScriptableObject());
197                 if (includeOldValue && attributeOldValue_) {
198                     mutationRecord.setOldValue(event.getValue());
199                 }
200 
201                 final Window window = getWindow();
202                 final HtmlPage owningPage = (HtmlPage) window.getDocument().getPage();
203                 final JavaScriptEngine jsEngine =
204                         (JavaScriptEngine) window.getWebWindow().getWebClient().getJavaScriptEngine();
205                 jsEngine.addPostponedAction(new PostponedAction(owningPage, actionTitle) {
206                     @Override
207                     public void execute() {
208                         final Scriptable array = JavaScriptEngine.newArray(scope, new Object[] {mutationRecord});
209                         jsEngine.callFunction(owningPage, function_,
210                                 scope, MutationObserver.this, new Object[] {array});
211                     }
212                 });
213             }
214         }
215     }
216 }