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