1
2
3
4
5
6
7
8
9
10
11
12
13
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
42
43
44
45
46
47
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
64
65
66 @JsxConstructor
67 @JsxConstructorAlias(value = {CHROME, EDGE}, alias = "WebKitMutationObserver")
68 public void jsConstructor(final Function function) {
69 function_ = function;
70 }
71
72
73
74
75
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
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
123
124
125 @JsxFunction
126 public Scriptable takeRecords() {
127 return JavaScriptEngine.newArray(getParentScope(), 0);
128 }
129
130
131
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
164
165 @Override
166 public void attributeAdded(final HtmlAttributeChangeEvent event) {
167 attributeChanged(event, "MutationObserver.attributeAdded", false);
168 }
169
170
171
172
173 @Override
174 public void attributeRemoved(final HtmlAttributeChangeEvent event) {
175 attributeChanged(event, "MutationObserver.attributeRemoved", true);
176 }
177
178
179
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 }