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