View Javadoc
1   /*
2    * Copyright (c) 2002-2017 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    * http://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 com.gargoylesoftware.htmlunit.javascript.host.event;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_CALL_RESULT_IS_LAST_RETURN_VALUE;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.JS_EVENT_WINDOW_EXECUTE_IF_DITACHED;
19  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
20  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
21  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.FF;
22  import static com.gargoylesoftware.htmlunit.javascript.configuration.SupportedBrowser.IE;
23  
24  import java.io.IOException;
25  import java.util.ArrayList;
26  import java.util.List;
27  
28  import org.apache.commons.lang3.StringUtils;
29  import org.w3c.dom.Document;
30  
31  import com.gargoylesoftware.htmlunit.ScriptResult;
32  import com.gargoylesoftware.htmlunit.html.DomDocumentFragment;
33  import com.gargoylesoftware.htmlunit.html.DomElement;
34  import com.gargoylesoftware.htmlunit.html.DomNode;
35  import com.gargoylesoftware.htmlunit.javascript.SimpleScriptable;
36  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxClass;
37  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxConstructor;
38  import com.gargoylesoftware.htmlunit.javascript.configuration.JsxFunction;
39  import com.gargoylesoftware.htmlunit.javascript.host.Window;
40  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
41  
42  import net.sourceforge.htmlunit.corejs.javascript.Context;
43  import net.sourceforge.htmlunit.corejs.javascript.Function;
44  import net.sourceforge.htmlunit.corejs.javascript.Scriptable;
45  
46  /**
47   * A JavaScript object for {@code EventTarget}.
48   *
49   * @author Ahmed Ashour
50   */
51  @JsxClass({CHROME, FF, EDGE})
52  @JsxClass(isJSObject = false, value = IE)
53  public class EventTarget extends SimpleScriptable {
54  
55      private EventListenersContainer eventListenersContainer_;
56  
57      /**
58       * Default constructor.
59       */
60      @JsxConstructor
61      public EventTarget() {
62      }
63  
64      /**
65       * Allows the registration of event listeners on the event target.
66       * @param type the event type to listen for (like "click")
67       * @param listener the event listener
68       * @param useCapture If {@code true}, indicates that the user wishes to initiate capture
69       * @see <a href="https://developer.mozilla.org/en-US/docs/DOM/element.addEventListener">Mozilla documentation</a>
70       */
71      @JsxFunction
72      public void addEventListener(final String type, final Scriptable listener, final boolean useCapture) {
73          getEventListenersContainer().addEventListener(type, listener, useCapture);
74      }
75  
76      /**
77       * Gets the container for event listeners.
78       * @return the container (newly created if needed)
79       */
80      public final EventListenersContainer getEventListenersContainer() {
81          if (eventListenersContainer_ == null) {
82              eventListenersContainer_ = new EventListenersContainer(this);
83          }
84          return eventListenersContainer_;
85      }
86  
87      /**
88       * Executes the event on this object only (needed for instance for onload on (i)frame tags).
89       * @param event the event
90       * @return the result
91       * @see #fireEvent(Event)
92       */
93      public ScriptResult executeEventLocally(final Event event) {
94          final EventListenersContainer eventListenersContainer = getEventListenersContainer();
95          if (eventListenersContainer != null) {
96              final Window window = getWindow();
97              final Object[] args = new Object[] {event};
98  
99              // handlers declared as property on a node don't receive the event as argument for IE
100             final Object[] propHandlerArgs = args;
101 
102             final Event previousEvent = window.getCurrentEvent();
103             window.setCurrentEvent(event);
104             try {
105                 return eventListenersContainer.executeListeners(event, args, propHandlerArgs);
106             }
107             finally {
108                 window.setCurrentEvent(previousEvent); // reset event
109             }
110         }
111         return null;
112     }
113 
114     /**
115      * Fires the event on the node with capturing and bubbling phase.
116      * @param event the event
117      * @return the result
118      */
119     public ScriptResult fireEvent(final Event event) {
120         final Window window = getWindow();
121         final Object[] args = new Object[] {event};
122 
123         event.startFire();
124         ScriptResult result = null;
125         final Event previousEvent = window.getCurrentEvent();
126         window.setCurrentEvent(event);
127 
128         try {
129             // window's listeners
130             final EventListenersContainer windowsListeners = window.getEventListenersContainer();
131 
132             // capturing phase
133             event.setEventPhase(Event.CAPTURING_PHASE);
134             final boolean windowEventIfDetached = getBrowserVersion().hasFeature(JS_EVENT_WINDOW_EXECUTE_IF_DITACHED);
135 
136             boolean isAttached = false;
137             for (DomNode node = getDomNodeOrNull(); node != null; node = node.getParentNode()) {
138                 if (node instanceof Document || node instanceof DomDocumentFragment) {
139                     isAttached = true;
140                     break;
141                 }
142             }
143 
144             if (isAttached || windowEventIfDetached) {
145                 result = windowsListeners.executeCapturingListeners(event, args);
146                 if (event.isPropagationStopped()) {
147                     return result;
148                 }
149             }
150             final List<EventTarget> eventTargetList = new ArrayList<>();
151             EventTarget eventTarget = this;
152             while (eventTarget != null) {
153                 if (isAttached) {
154                     eventTargetList.add(eventTarget);
155                 }
156                 final DomNode domNode = eventTarget.getDomNodeOrNull();
157                 eventTarget = null;
158                 if (domNode != null && domNode.getParentNode() != null) {
159                     eventTarget = (EventTarget) domNode.getParentNode().getScriptableObject();
160                 }
161             }
162 
163             final boolean ie = getBrowserVersion().hasFeature(JS_CALL_RESULT_IS_LAST_RETURN_VALUE);
164             for (int i = eventTargetList.size() - 1; i >= 0; i--) {
165                 final EventTarget jsNode = eventTargetList.get(i);
166                 final EventListenersContainer elc = jsNode.eventListenersContainer_;
167                 if (elc != null && isAttached) {
168                     final ScriptResult r = elc.executeCapturingListeners(event, args);
169                     result = ScriptResult.combine(r, result, ie);
170                     if (event.isPropagationStopped()) {
171                         return result;
172                     }
173                 }
174             }
175 
176             // handlers declared as property on a node don't receive the event as argument for IE
177             final Object[] propHandlerArgs = args;
178 
179             // bubbling phase
180             event.setEventPhase(Event.AT_TARGET);
181             eventTarget = this;
182             while (eventTarget != null) {
183                 final EventTarget jsNode = eventTarget;
184                 final EventListenersContainer elc = jsNode.eventListenersContainer_;
185                 if (elc != null && !(jsNode instanceof Window) && (isAttached || !(jsNode instanceof HTMLElement))) {
186                     final ScriptResult r = elc.executeBubblingListeners(event, args, propHandlerArgs);
187                     result = ScriptResult.combine(r, result, ie);
188                     if (event.isPropagationStopped()) {
189                         return result;
190                     }
191                 }
192                 final DomNode domNode = eventTarget.getDomNodeOrNull();
193                 eventTarget = null;
194                 if (domNode != null && domNode.getParentNode() != null) {
195                     eventTarget = (EventTarget) domNode.getParentNode().getScriptableObject();
196                 }
197                 event.setEventPhase(Event.BUBBLING_PHASE);
198             }
199 
200             if (isAttached || windowEventIfDetached) {
201                 final ScriptResult r = windowsListeners.executeBubblingListeners(event, args, propHandlerArgs);
202                 result = ScriptResult.combine(r, result, ie);
203             }
204         }
205         finally {
206             event.endFire();
207             window.setCurrentEvent(previousEvent); // reset event
208         }
209 
210         return result;
211     }
212 
213     /**
214      * Returns {@code true} if there are any event handlers for the specified event.
215      * @param eventName the event name (e.g. "onclick")
216      * @return {@code true} if there are any event handlers for the specified event, {@code false} otherwise
217      */
218     public boolean hasEventHandlers(final String eventName) {
219         if (eventListenersContainer_ == null) {
220             return false;
221         }
222         return eventListenersContainer_.hasEventListeners(StringUtils.substring(eventName, 2));
223     }
224 
225     /**
226      * Returns the specified event handler.
227      * @param eventType the event type (e.g. "click")
228      * @return the handler function, or {@code null} if the property is null or not a function
229      */
230     public Function getEventHandler(final String eventType) {
231         return getEventListenersContainer().getEventHandler(eventType);
232     }
233 
234     /**
235      * Dispatches an event into the event system (standards-conformant browsers only). See
236      * <a href="https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent">the Gecko
237      * DOM reference</a> for more information.
238      *
239      * @param event the event to be dispatched
240      * @return {@code false} if at least one of the event handlers which handled the event
241      *         called <tt>preventDefault</tt>; {@code true} otherwise
242      */
243     @JsxFunction
244     public boolean dispatchEvent(final Event event) {
245         event.setTarget(this);
246         final DomElement element = (DomElement) getDomNodeOrNull();
247         ScriptResult result = null;
248         if (event.getType().equals(MouseEvent.TYPE_CLICK)) {
249             try {
250                 element.click(event, true);
251             }
252             catch (final IOException e) {
253                 throw Context.reportRuntimeError("Error calling click(): " + e.getMessage());
254             }
255         }
256         else {
257             result = fireEvent(event);
258         }
259         return !event.isAborted(result);
260     }
261 
262     /**
263      * Allows the removal of event listeners on the event target.
264      * @param type the event type to listen for (like "click")
265      * @param listener the event listener
266      * @param useCapture If {@code true}, indicates that the user wishes to initiate capture (not yet implemented)
267      * @see <a href="https://developer.mozilla.org/en-US/docs/DOM/element.removeEventListener">Mozilla
268      * documentation</a>
269      */
270     @JsxFunction
271     public void removeEventListener(final String type, final Scriptable listener, final boolean useCapture) {
272         getEventListenersContainer().removeEventListener(type, listener, useCapture);
273     }
274 
275     /**
276      * Defines an event handler (or maybe any other object).
277      * @param eventName the event name (e.g. "click")
278      * @param value the property ({@code null} to reset it)
279      */
280     public void setEventHandler(final String eventName, final Object value) {
281         final EventListenersContainer container;
282         if (isEventHandlerOnWindow()) {
283             container = getWindow().getEventListenersContainer();
284         }
285         else {
286             container = getEventListenersContainer();
287         }
288         container.setEventHandler(eventName, value);
289     }
290 
291     /**
292      * Is setting event handler property, at window-level.
293      * @return whether the event handler to be set at window-level
294      */
295     protected boolean isEventHandlerOnWindow() {
296         return false;
297     }
298 
299     /**
300      * Clears the event listener container.
301      */
302     protected void clearEventListenersContainer() {
303         eventListenersContainer_ = null;
304     }
305 }