View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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.corejs.javascript.VarScope;
25  import org.htmlunit.html.CharacterDataChangeEvent;
26  import org.htmlunit.html.CharacterDataChangeListener;
27  import org.htmlunit.html.HtmlAttributeChangeEvent;
28  import org.htmlunit.html.HtmlAttributeChangeListener;
29  import org.htmlunit.html.HtmlElement;
30  import org.htmlunit.html.HtmlPage;
31  import org.htmlunit.javascript.HtmlUnitScriptable;
32  import org.htmlunit.javascript.JavaScriptEngine;
33  import org.htmlunit.javascript.PostponedAction;
34  import org.htmlunit.javascript.configuration.JsxClass;
35  import org.htmlunit.javascript.configuration.JsxConstructor;
36  import org.htmlunit.javascript.configuration.JsxConstructorAlias;
37  import org.htmlunit.javascript.configuration.JsxFunction;
38  import org.htmlunit.javascript.host.Window;
39  
40  /**
41   * A JavaScript object for {@code MutationObserver}.
42   *
43   * @author Ahmed Ashour
44   * @author Ronald Brill
45   * @author Atsushi Nakagawa
46   *
47   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver">MDN Documentation</a>
48   */
49  @JsxClass
50  public class MutationObserver extends HtmlUnitScriptable implements HtmlAttributeChangeListener,
51          CharacterDataChangeListener {
52  
53      private Function function_;
54      private Node node_;
55      private boolean attributes_;
56      private boolean attributeOldValue_;
57      private NativeArray attributeFilter_;
58      private boolean characterData_;
59      private boolean characterDataOldValue_;
60      private boolean subtree_;
61  
62      /**
63       * Creates an instance.
64       * @param function the function to observe
65       */
66      @JsxConstructor
67      @JsxConstructorAlias(value = {CHROME, EDGE}, alias = "WebKitMutationObserver")
68      public void jsConstructor(final Function function) {
69          function_ = function;
70      }
71  
72      /**
73       * Registers the {@link MutationObserver} instance to receive notifications of DOM mutations on the specified node.
74       * @param node the node
75       * @param options the options
76       */
77      @JsxFunction
78      public void observe(final Node node, final NativeObject options) {
79          if (node == null) {
80              throw JavaScriptEngine.typeError("Node is undefined");
81          }
82          if (options == null) {
83              throw JavaScriptEngine.typeError("Options is undefined");
84          }
85  
86          node_ = node;
87          attributes_ = Boolean.TRUE.equals(options.get("attributes"));
88          attributeOldValue_ = Boolean.TRUE.equals(options.get("attributeOldValue"));
89          characterData_ = Boolean.TRUE.equals(options.get("characterData"));
90          characterDataOldValue_ = Boolean.TRUE.equals(options.get("characterDataOldValue"));
91          subtree_ = Boolean.TRUE.equals(options.get("subtree"));
92          attributeFilter_ = (NativeArray) options.get("attributeFilter");
93  
94          final boolean childList = Boolean.TRUE.equals(options.get("childList"));
95  
96          if (!attributes_ && !childList && !characterData_) {
97              throw JavaScriptEngine.typeError("One of childList, attributes, or characterData must be set");
98          }
99  
100         if (attributes_ && node_.getDomNodeOrDie() instanceof HtmlElement) {
101             ((HtmlElement) node_.getDomNodeOrDie()).addHtmlAttributeChangeListener(this);
102         }
103         if (characterData_) {
104             node.getDomNodeOrDie().addCharacterDataChangeListener(this);
105         }
106     }
107 
108     /**
109      * Stops the MutationObserver instance from receiving notifications of DOM mutations.
110      */
111     @JsxFunction
112     public void disconnect() {
113         if (attributes_ && node_.getDomNodeOrDie() instanceof HtmlElement) {
114             ((HtmlElement) node_.getDomNodeOrDie()).removeHtmlAttributeChangeListener(this);
115         }
116         if (characterData_) {
117             node_.getDomNodeOrDie().removeCharacterDataChangeListener(this);
118         }
119     }
120 
121     /**
122      * Empties the MutationObserver instance's record queue and returns what was in there.
123      * @return a {@link NativeArray} of {@link MutationRecord}s
124      */
125     @JsxFunction
126     public Scriptable takeRecords() {
127         return JavaScriptEngine.newArray(getParentScope(), 0);
128     }
129 
130     /**
131      * {@inheritDoc}
132      */
133     @Override
134     public void characterDataChanged(final CharacterDataChangeEvent event) {
135         final HtmlUnitScriptable target = event.getCharacterData().getScriptableObject();
136         if (subtree_ || target == node_) {
137             final MutationRecord mutationRecord = new MutationRecord();
138             final VarScope scope = getParentScope();
139             mutationRecord.setParentScope(scope);
140             mutationRecord.setPrototype(getPrototype(mutationRecord.getClass()));
141 
142             mutationRecord.setType("characterData");
143             mutationRecord.setTarget(target);
144             if (characterDataOldValue_) {
145                 mutationRecord.setOldValue(event.getOldValue());
146             }
147 
148             final Window window = getWindow();
149             final HtmlPage owningPage = (HtmlPage) window.getDocument().getPage();
150             final JavaScriptEngine jsEngine =
151                     (JavaScriptEngine) window.getWebWindow().getWebClient().getJavaScriptEngine();
152             jsEngine.addPostponedAction(new PostponedAction(owningPage, "MutationObserver.characterDataChanged") {
153                 @Override
154                 public void execute() {
155                     final Scriptable array = JavaScriptEngine.newArray(scope, new Object[] {mutationRecord});
156                     jsEngine.callFunction(owningPage, function_, scope, MutationObserver.this, new Object[] {array});
157                 }
158             });
159         }
160     }
161 
162     /**
163      * {@inheritDoc}
164      */
165     @Override
166     public void attributeAdded(final HtmlAttributeChangeEvent event) {
167         attributeChanged(event, "MutationObserver.attributeAdded", false);
168     }
169 
170     /**
171      * {@inheritDoc}
172      */
173     @Override
174     public void attributeRemoved(final HtmlAttributeChangeEvent event) {
175         attributeChanged(event, "MutationObserver.attributeRemoved", true);
176     }
177 
178     /**
179      * {@inheritDoc}
180      */
181     @Override
182     public void attributeReplaced(final HtmlAttributeChangeEvent event) {
183         attributeChanged(event, "MutationObserver.attributeReplaced", true);
184     }
185 
186     private void attributeChanged(final HtmlAttributeChangeEvent event, final String actionTitle,
187                         final boolean includeOldValue) {
188         final HtmlElement target = event.getHtmlElement();
189         if (subtree_ || target == node_.getDomNodeOrDie()) {
190             final String attributeName = event.getName();
191             if (attributeFilter_ == null || attributeFilter_.contains(attributeName)) {
192                 final MutationRecord mutationRecord = new MutationRecord();
193                 final VarScope scope = getParentScope();
194                 mutationRecord.setParentScope(scope);
195                 mutationRecord.setPrototype(getPrototype(mutationRecord.getClass()));
196 
197                 mutationRecord.setAttributeName(attributeName);
198                 mutationRecord.setType("attributes");
199                 mutationRecord.setTarget(target.getScriptableObject());
200                 if (includeOldValue && attributeOldValue_) {
201                     mutationRecord.setOldValue(event.getValue());
202                 }
203 
204                 final Window window = getWindow();
205                 final HtmlPage owningPage = (HtmlPage) window.getDocument().getPage();
206                 final JavaScriptEngine jsEngine =
207                         (JavaScriptEngine) window.getWebWindow().getWebClient().getJavaScriptEngine();
208                 jsEngine.addPostponedAction(new PostponedAction(owningPage, actionTitle) {
209                     @Override
210                     public void execute() {
211                         final Scriptable array = JavaScriptEngine.newArray(scope, new Object[] {mutationRecord});
212                         jsEngine.callFunction(owningPage, function_,
213                                 scope, MutationObserver.this, new Object[] {array});
214                     }
215                 });
216             }
217         }
218     }
219 }