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