1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host.event;
16
17 import java.io.Serializable;
18 import java.util.ArrayList;
19 import java.util.Collections;
20 import java.util.List;
21 import java.util.Locale;
22 import java.util.concurrent.ConcurrentHashMap;
23 import java.util.concurrent.ConcurrentMap;
24
25 import org.apache.commons.logging.Log;
26 import org.apache.commons.logging.LogFactory;
27 import org.htmlunit.ScriptResult;
28 import org.htmlunit.corejs.javascript.Function;
29 import org.htmlunit.corejs.javascript.NativeObject;
30 import org.htmlunit.corejs.javascript.Scriptable;
31 import org.htmlunit.corejs.javascript.ScriptableObject;
32 import org.htmlunit.corejs.javascript.TopLevel;
33 import org.htmlunit.corejs.javascript.VarScope;
34 import org.htmlunit.corejs.javascript.WithScope;
35 import org.htmlunit.html.DomNode;
36 import org.htmlunit.html.HtmlPage;
37 import org.htmlunit.javascript.JavaScriptEngine;
38 import org.htmlunit.javascript.host.Window;
39 import org.htmlunit.javascript.host.html.HTMLDocument;
40 import org.htmlunit.javascript.host.html.HTMLElement;
41
42
43
44
45
46
47
48
49
50
51
52 public class EventListenersContainer implements Serializable {
53
54 private static final Log LOG = LogFactory.getLog(EventListenersContainer.class);
55
56
57
58
59
60
61 private final ConcurrentMap<String, TypeContainer> typeContainers_ = new ConcurrentHashMap<>();
62 private final EventTarget jsNode_;
63
64 private record TypeContainer(List<Scriptable> capturingListeners_, List<Scriptable> bubblingListeners_,
65 List<Scriptable> atTargetListeners_, Function handler_) implements Serializable {
66 public static final TypeContainer EMPTY = new TypeContainer();
67
68
69
70 private static final Scriptable EVENT_HANDLER_PLACEHOLDER = null;
71
72 TypeContainer() {
73 this(Collections.emptyList(), Collections.emptyList(), Collections.emptyList(), null);
74 }
75
76 List<Scriptable> getListeners(final int eventPhase) {
77 return switch (eventPhase) {
78 case Event.CAPTURING_PHASE -> capturingListeners_;
79 case Event.AT_TARGET -> atTargetListeners_;
80 case Event.BUBBLING_PHASE -> bubblingListeners_;
81 default -> throw new UnsupportedOperationException("eventPhase: " + eventPhase);
82 };
83 }
84
85 public TypeContainer setPropertyHandler(final Function propertyHandler) {
86 if (propertyHandler != null) {
87
88
89 if (handler_ != null) {
90 if (propertyHandler == handler_) {
91 return this;
92 }
93 return withPropertyHandler(propertyHandler);
94 }
95
96
97 return withPropertyHandler(propertyHandler).addListener(EVENT_HANDLER_PLACEHOLDER, false);
98 }
99 if (handler_ == null) {
100 return this;
101 }
102 return removeListener(EVENT_HANDLER_PLACEHOLDER, false).withPropertyHandler(null);
103 }
104
105 private TypeContainer withPropertyHandler(final Function propertyHandler) {
106 return new TypeContainer(capturingListeners_, bubblingListeners_, atTargetListeners_, propertyHandler);
107 }
108
109 public TypeContainer addListener(final Scriptable listener, final boolean useCapture) {
110 List<Scriptable> capturingListeners = capturingListeners_;
111 List<Scriptable> bubblingListeners = bubblingListeners_;
112 final List<Scriptable> listeners = useCapture ? capturingListeners : bubblingListeners;
113
114 if (listeners.contains(listener)) {
115 return this;
116 }
117
118 List<Scriptable> newListeners = new ArrayList<>(listeners.size() + 1);
119 newListeners.addAll(listeners);
120 newListeners.add(listener);
121 newListeners = Collections.unmodifiableList(newListeners);
122
123 if (useCapture) {
124 capturingListeners = newListeners;
125 }
126 else {
127 bubblingListeners = newListeners;
128 }
129
130 List<Scriptable> atTargetListeners = new ArrayList<>(atTargetListeners_.size() + 1);
131 atTargetListeners.addAll(atTargetListeners_);
132 atTargetListeners.add(listener);
133 atTargetListeners = Collections.unmodifiableList(atTargetListeners);
134
135 return new TypeContainer(capturingListeners, bubblingListeners, atTargetListeners, handler_);
136 }
137
138 public TypeContainer removeListener(final Scriptable listener, final boolean useCapture) {
139 List<Scriptable> capturingListeners = capturingListeners_;
140 List<Scriptable> bubblingListeners = bubblingListeners_;
141 final List<Scriptable> listeners = useCapture ? capturingListeners : bubblingListeners;
142
143 final int idx = listeners.indexOf(listener);
144 if (idx < 0) {
145 return this;
146 }
147
148 List<Scriptable> newListeners = new ArrayList<>(listeners);
149 newListeners.remove(idx);
150 newListeners = Collections.unmodifiableList(newListeners);
151
152 if (useCapture) {
153 capturingListeners = newListeners;
154 }
155 else {
156 bubblingListeners = newListeners;
157 }
158
159 List<Scriptable> atTargetListeners = new ArrayList<>(atTargetListeners_);
160 atTargetListeners.remove(listener);
161 atTargetListeners = Collections.unmodifiableList(atTargetListeners);
162
163 return new TypeContainer(capturingListeners, bubblingListeners, atTargetListeners, handler_);
164 }
165
166
167 @Override
168 protected TypeContainer clone() {
169 return new TypeContainer(capturingListeners_, bubblingListeners_, atTargetListeners_, handler_);
170 }
171 }
172
173
174
175
176
177
178 public EventListenersContainer(final EventTarget jsNode) {
179 jsNode_ = jsNode;
180 }
181
182
183
184
185
186
187
188
189
190 public boolean addEventListener(final String type, final Scriptable listener, final boolean useCapture) {
191 if (listener == null) {
192 return true;
193 }
194
195 final boolean[] added = {false};
196 typeContainers_.compute(type.toLowerCase(Locale.ROOT), (k, container) -> {
197 if (container == null) {
198 container = TypeContainer.EMPTY;
199 }
200 final TypeContainer newContainer = container.addListener(listener, useCapture);
201 added[0] = newContainer != container;
202 return newContainer;
203 });
204
205 if (!added[0]) {
206 if (LOG.isDebugEnabled()) {
207 LOG.debug(type + " listener already registered, skipping it (" + listener + ")");
208 }
209 return false;
210 }
211 return true;
212 }
213
214 private TypeContainer getTypeContainer(final String type) {
215 final String typeLC = type.toLowerCase(Locale.ROOT);
216 return typeContainers_.getOrDefault(typeLC, TypeContainer.EMPTY);
217 }
218
219
220
221
222
223
224
225
226 public List<Scriptable> getListeners(final String eventType, final boolean useCapture) {
227 return getTypeContainer(eventType).getListeners(useCapture ? Event.CAPTURING_PHASE : Event.BUBBLING_PHASE);
228 }
229
230
231
232
233
234
235
236
237 void removeEventListener(final String eventType, final Scriptable listener, final boolean useCapture) {
238 if (listener == null) {
239 return;
240 }
241
242 typeContainers_.computeIfPresent(eventType.toLowerCase(Locale.ROOT),
243 (k, container) -> container.removeListener(listener, useCapture));
244 }
245
246
247
248
249
250
251
252 public void setEventHandler(final String eventType, final Object value) {
253 final Function handler;
254
255
256 if (JavaScriptEngine.isUndefined(value) || !(value instanceof Function)) {
257 handler = null;
258 }
259 else {
260 handler = (Function) value;
261 }
262
263 typeContainers_.compute(eventType.toLowerCase(Locale.ROOT), (k, container) -> {
264 if (container == null) {
265 container = TypeContainer.EMPTY;
266 }
267 return container.setPropertyHandler(handler);
268 });
269 }
270
271 private void executeEventListeners(final int eventPhase, final Event event, final Object[] args) {
272 final DomNode node = jsNode_.getDomNodeOrNull();
273
274 if (node != null && !node.handles(event)) {
275 return;
276 }
277
278 final TypeContainer container = getTypeContainer(event.getType());
279 final List<Scriptable> listeners = container.getListeners(eventPhase);
280 if (!listeners.isEmpty()) {
281 event.setCurrentTarget(jsNode_);
282
283 final HtmlPage page;
284 if (jsNode_ instanceof Window) {
285 page = (HtmlPage) jsNode_.getDomNodeOrDie();
286 }
287 else {
288 Scriptable scriptableObj = null;
289 final VarScope parentScope = jsNode_.getParentScope();
290 if (parentScope instanceof TopLevel topLevel) {
291 scriptableObj = topLevel.getGlobalThis();
292 }
293 else if (parentScope instanceof WithScope withScope) {
294 scriptableObj = withScope.getObject();
295 }
296
297 if (scriptableObj instanceof Window window) {
298 page = (HtmlPage) window.getDomNodeOrDie();
299 }
300 else if (scriptableObj instanceof HTMLDocument document) {
301 page = document.getPage();
302 }
303 else if (scriptableObj != null) {
304 page = ((HTMLElement) scriptableObj).getDomNodeOrDie().getHtmlPageOrNull();
305 }
306 else {
307 page = null;
308 throw new UnsupportedOperationException("TODO ");
309 }
310 }
311
312
313 for (Scriptable listener : listeners) {
314 boolean isPropertyHandler = false;
315 if (listener == TypeContainer.EVENT_HANDLER_PLACEHOLDER) {
316 listener = container.handler_;
317 isPropertyHandler = true;
318 }
319 Function function = null;
320 Scriptable thisObject = null;
321 if (listener instanceof Function function2) {
322 function = function2;
323 thisObject = jsNode_;
324 }
325 else if (listener instanceof NativeObject) {
326 final Object handleEvent = ScriptableObject.getProperty(listener, "handleEvent");
327 if (handleEvent instanceof Function function1) {
328 function = function1;
329 thisObject = listener;
330 }
331 }
332 if (function != null) {
333 final ScriptResult result =
334 page.executeJavaScriptFunction(function, thisObject, args, node);
335
336 if (isPropertyHandler && !ScriptResult.isUndefined(result)) {
337 event.handlePropertyHandlerReturnValue(result.getJavaScriptResult());
338 }
339 }
340 if (event.isImmediatePropagationStopped()) {
341 return;
342 }
343 }
344 }
345 }
346
347
348
349
350
351
352
353 public void executeBubblingListeners(final Event event, final Object[] args) {
354 executeEventListeners(Event.BUBBLING_PHASE, event, args);
355 }
356
357
358
359
360
361
362
363 public void executeCapturingListeners(final Event event, final Object[] args) {
364 executeEventListeners(Event.CAPTURING_PHASE, event, args);
365 }
366
367
368
369
370
371
372
373 public void executeAtTargetListeners(final Event event, final Object[] args) {
374 executeEventListeners(Event.AT_TARGET, event, args);
375 }
376
377
378
379
380
381
382
383 public Function getEventHandler(final String eventType) {
384 return getTypeContainer(eventType).handler_;
385 }
386
387
388
389
390
391
392
393 boolean hasEventListeners(final String eventType) {
394 return !getTypeContainer(eventType).atTargetListeners_.isEmpty();
395 }
396
397
398
399
400 @Override
401 public String toString() {
402 return getClass().getSimpleName() + "[node=" + jsNode_ + " handlers=" + typeContainers_.keySet() + "]";
403 }
404 }