1 /*
2 * Copyright (c) 2002-2025 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.event;
16
17 import java.io.IOException;
18 import java.util.ArrayList;
19 import java.util.List;
20
21 import org.apache.commons.lang3.StringUtils;
22 import org.htmlunit.Page;
23 import org.htmlunit.ScriptResult;
24 import org.htmlunit.corejs.javascript.Function;
25 import org.htmlunit.corejs.javascript.Scriptable;
26 import org.htmlunit.html.DomElement;
27 import org.htmlunit.html.DomNode;
28 import org.htmlunit.html.HtmlElement;
29 import org.htmlunit.html.HtmlLabel;
30 import org.htmlunit.javascript.HtmlUnitScriptable;
31 import org.htmlunit.javascript.JavaScriptEngine;
32 import org.htmlunit.javascript.configuration.JsxClass;
33 import org.htmlunit.javascript.configuration.JsxConstructor;
34 import org.htmlunit.javascript.configuration.JsxFunction;
35 import org.htmlunit.javascript.host.Window;
36 import org.htmlunit.javascript.host.dom.Document;
37
38 /**
39 * A JavaScript object for {@code EventTarget}.
40 *
41 * @author Ahmed Ashour
42 * @author Ronald Brill
43 * @author Atsushi Nakagawa
44 */
45 @JsxClass
46 public class EventTarget extends HtmlUnitScriptable {
47
48 private EventListenersContainer eventListenersContainer_;
49
50 /**
51 * JavaScript constructor.
52 */
53 @JsxConstructor
54 public void jsConstructor() {
55 // nothing to do
56 }
57
58 /**
59 * Allows the registration of event listeners on the event target.
60 * @param type the event type to listen for (like "click")
61 * @param listener the event listener
62 * @param useCapture If {@code true}, indicates that the user wishes to initiate capture
63 * @see <a href="https://developer.mozilla.org/en-US/docs/DOM/element.addEventListener">Mozilla documentation</a>
64 */
65 @JsxFunction
66 public void addEventListener(final String type, final Scriptable listener, final boolean useCapture) {
67 getEventListenersContainer().addEventListener(type, listener, useCapture);
68 }
69
70 /**
71 * Gets the container for event listeners.
72 * @return the container (newly created if needed)
73 */
74 public final EventListenersContainer getEventListenersContainer() {
75 if (eventListenersContainer_ == null) {
76 eventListenersContainer_ = new EventListenersContainer(this);
77 }
78 return eventListenersContainer_;
79 }
80
81 /**
82 * Executes the event on this object only (needed for instance for onload on (i)frame tags).
83 * @param event the event
84 * @see #fireEvent(Event)
85 */
86 public void executeEventLocally(final Event event) {
87 final EventListenersContainer eventListenersContainer = getEventListenersContainer();
88 final Window window = getWindow();
89 final Object[] args = {event};
90
91 final Event previousEvent = window.getCurrentEvent();
92 window.setCurrentEvent(event);
93 try {
94 event.setEventPhase(Event.AT_TARGET);
95 eventListenersContainer.executeAtTargetListeners(event, args);
96 }
97 finally {
98 window.setCurrentEvent(previousEvent); // reset event
99 }
100 }
101
102 /**
103 * Fires the event on the node with capturing and bubbling phase.
104 * @param event the event
105 * @return the result
106 */
107 public ScriptResult fireEvent(final Event event) {
108 final Window window = getWindow();
109
110 event.startFire();
111 final Event previousEvent = window.getCurrentEvent();
112 window.setCurrentEvent(event);
113
114 try {
115 // These can be null if we aren't tied to a DOM node
116 final DomNode ourNode = getDomNodeOrNull();
117 final DomNode ourParentNode = (ourNode != null) ? ourNode.getParentNode() : null;
118
119 // Determine the propagation path which is fixed here and not affected by
120 // DOM tree modification from intermediate listeners (tested in Chrome)
121 final List<EventTarget> propagationPath = new ArrayList<>();
122
123 // We're added to the propagation path first
124 propagationPath.add(this);
125
126 // Then add all our parents if we have any (pure JS object such as XMLHttpRequest
127 // and MessagePort, etc. will not have any parents)
128 for (DomNode parent = ourParentNode; parent != null; parent = parent.getParentNode()) {
129 // scroll does not bubble into the document/window
130 if (Event.TYPE_SCROLL.equals(event.getType()) && parent instanceof Page) {
131 break;
132 }
133
134 propagationPath.add(parent.getScriptableObject());
135 }
136
137 // The load event has some unnatural behavior that we need to handle specially
138 // The load event for other elements target that element and but path only
139 // up to Document and not Window, so do nothing here
140 // (see Note in https://www.w3.org/TR/DOM-Level-3-Events/#event-type-load)
141 if (!Event.TYPE_LOAD.equals(event.getType())) {
142 // Add Window if the the propagation path reached Document
143 if (propagationPath.get(propagationPath.size() - 1) instanceof Document) {
144 propagationPath.add(window);
145 }
146 }
147
148 // capturing phase
149 event.setEventPhase(Event.CAPTURING_PHASE);
150
151 for (int i = propagationPath.size() - 1; i >= 1; i--) {
152 final EventTarget jsNode = propagationPath.get(i);
153 final EventListenersContainer elc = jsNode.eventListenersContainer_;
154 if (elc != null) {
155 elc.executeCapturingListeners(event, new Object[] {event});
156 if (event.isPropagationStopped()) {
157 return new ScriptResult(null);
158 }
159 }
160 }
161
162 // at target phase
163 event.setEventPhase(Event.AT_TARGET);
164
165 if (!propagationPath.isEmpty()) {
166 // Note: This element is not always the same as event.getTarget():
167 // e.g. the 'load' event targets Document but "at target" is on Window.
168 final EventTarget jsNode = propagationPath.get(0);
169 final EventListenersContainer elc = jsNode.eventListenersContainer_;
170 if (elc != null) {
171 elc.executeAtTargetListeners(event, new Object[] {event});
172 if (event.isPropagationStopped()) {
173 return new ScriptResult(null);
174 }
175 }
176 }
177
178 // bubbling phase
179 if (event.isBubbles()) {
180 // This belongs here inside the block because events that don't bubble never set
181 // eventPhase = 3 (tested in Chrome)
182 event.setEventPhase(Event.BUBBLING_PHASE);
183
184 final int size = propagationPath.size();
185 for (int i = 1; i < size; i++) {
186 final EventTarget jsNode = propagationPath.get(i);
187 final EventListenersContainer elc = jsNode.eventListenersContainer_;
188 if (elc != null) {
189 elc.executeBubblingListeners(event, new Object[] {event});
190 if (event.isPropagationStopped()) {
191 return new ScriptResult(null);
192 }
193 }
194 }
195 }
196
197 HtmlLabel label = null;
198 if (event.processLabelAfterBubbling()) {
199 for (DomNode parent = ourParentNode; parent != null; parent = parent.getParentNode()) {
200 if (parent instanceof HtmlLabel) {
201 label = (HtmlLabel) parent;
202 break;
203 }
204 }
205 }
206
207 if (label != null) {
208 final HtmlElement element = label.getLabeledElement();
209 if (element != null && element != getDomNodeOrNull()) {
210 try {
211 element.click(event.isShiftKey(), event.isCtrlKey(), event.isAltKey(), false, true, true, true);
212 }
213 catch (final IOException ignored) {
214 // ignore for now
215 }
216 }
217 }
218
219 }
220 finally {
221 event.endFire();
222 window.setCurrentEvent(previousEvent); // reset event
223 }
224
225 return new ScriptResult(null);
226 }
227
228 /**
229 * Returns {@code true} if there are any event handlers for the specified event.
230 * @param eventName the event name (e.g. "onclick")
231 * @return {@code true} if there are any event handlers for the specified event, {@code false} otherwise
232 */
233 public boolean hasEventHandlers(final String eventName) {
234 if (eventListenersContainer_ == null) {
235 return false;
236 }
237 return eventListenersContainer_.hasEventListeners(StringUtils.substring(eventName, 2));
238 }
239
240 /**
241 * Returns the specified event handler.
242 * @param eventType the event type (e.g. "click")
243 * @return the handler function, or {@code null} if the property is null or not a function
244 */
245 public Function getEventHandler(final String eventType) {
246 if (eventListenersContainer_ == null) {
247 return null;
248 }
249 return eventListenersContainer_.getEventHandler(eventType);
250 }
251
252 /**
253 * Dispatches an event into the event system (standards-conformant browsers only). See
254 * <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent">the Gecko
255 * DOM reference</a> for more information.
256 *
257 * @param event the event to be dispatched
258 * @return {@code false} if at least one of the event handlers which handled the event
259 * called <code>preventDefault</code>; {@code true} otherwise
260 */
261 @JsxFunction
262 public boolean dispatchEvent(final Event event) {
263 event.setTarget(this);
264
265 ScriptResult result = null;
266 final DomNode domNode = getDomNodeOrNull();
267 if (MouseEvent.TYPE_CLICK.equals(event.getType()) && (domNode instanceof DomElement)) {
268 try {
269 ((DomElement) domNode).click(event, event.isShiftKey(), event.isCtrlKey(), event.isAltKey(), true);
270 }
271 catch (final IOException e) {
272 throw JavaScriptEngine.reportRuntimeError("Error calling click(): " + e.getMessage());
273 }
274 }
275 else {
276 result = fireEvent(event);
277 }
278 return !event.isAborted(result);
279 }
280
281 /**
282 * Allows the removal of event listeners on the event target.
283 * @param type the event type to listen for (like "click")
284 * @param listener the event listener
285 * @param useCapture If {@code true}, indicates that the user wishes to initiate capture (not yet implemented)
286 * @see <a href="https://developer.mozilla.org/en-US/docs/DOM/element.removeEventListener">Mozilla
287 * documentation</a>
288 */
289 @JsxFunction
290 public void removeEventListener(final String type, final Scriptable listener, final boolean useCapture) {
291 if (eventListenersContainer_ == null) {
292 return;
293 }
294 eventListenersContainer_.removeEventListener(type, listener, useCapture);
295 }
296
297 /**
298 * Defines an event handler (or maybe any other object).
299 * @param eventName the event name (e.g. "click")
300 * @param value the property ({@code null} to reset it)
301 */
302 public void setEventHandler(final String eventName, final Object value) {
303 if (isEventHandlerOnWindow()) {
304 getWindow().getEventListenersContainer().setEventHandler(eventName, value);
305 return;
306 }
307 getEventListenersContainer().setEventHandler(eventName, value);
308 }
309
310 /**
311 * Is setting event handler property, at window-level.
312 * @return whether the event handler to be set at window-level
313 */
314 protected boolean isEventHandlerOnWindow() {
315 return false;
316 }
317
318 /**
319 * Clears the event listener container.
320 */
321 protected void clearEventListenersContainer() {
322 eventListenersContainer_ = null;
323 }
324 }