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