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 @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
62
63
64 @JsxConstructor
65 @JsxConstructorAlias(value = {CHROME, EDGE}, alias = "WebKitMutationObserver")
66 public void jsConstructor(final Function function) {
67 function_ = function;
68 }
69
70
71
72
73
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
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
121
122
123 @JsxFunction
124 public Scriptable takeRecords() {
125 return JavaScriptEngine.newArray(getParentScope(), 0);
126 }
127
128
129
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
162
163 @Override
164 public void attributeAdded(final HtmlAttributeChangeEvent event) {
165 attributeChanged(event, "MutationObserver.attributeAdded", false);
166 }
167
168
169
170
171 @Override
172 public void attributeRemoved(final HtmlAttributeChangeEvent event) {
173 attributeChanged(event, "MutationObserver.attributeRemoved", true);
174 }
175
176
177
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 }