View Javadoc
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.html;
16  
17  import static org.htmlunit.BrowserVersionFeatures.EVENT_CONTEXT_MENU_HAS_DETAIL_1;
18  import static org.htmlunit.BrowserVersionFeatures.EVENT_ONCLICK_USES_POINTEREVENT;
19  import static org.htmlunit.BrowserVersionFeatures.JS_AREA_WITHOUT_HREF_FOCUSABLE;
20  
21  import java.io.IOException;
22  import java.io.PrintWriter;
23  import java.io.Serializable;
24  import java.io.StringWriter;
25  import java.util.ArrayList;
26  import java.util.Collection;
27  import java.util.Comparator;
28  import java.util.Iterator;
29  import java.util.LinkedHashMap;
30  import java.util.List;
31  import java.util.Locale;
32  import java.util.Map;
33  import java.util.NoSuchElementException;
34  import java.util.Set;
35  
36  import org.apache.commons.logging.Log;
37  import org.apache.commons.logging.LogFactory;
38  import org.htmlunit.BrowserVersion;
39  import org.htmlunit.Page;
40  import org.htmlunit.ScriptResult;
41  import org.htmlunit.SgmlPage;
42  import org.htmlunit.WebClient;
43  import org.htmlunit.css.ComputedCssStyleDeclaration;
44  import org.htmlunit.css.CssStyleSheet;
45  import org.htmlunit.css.StyleElement;
46  import org.htmlunit.cssparser.dom.CSSStyleDeclarationImpl;
47  import org.htmlunit.cssparser.dom.Property;
48  import org.htmlunit.cssparser.parser.CSSException;
49  import org.htmlunit.cssparser.parser.selector.Selector;
50  import org.htmlunit.cssparser.parser.selector.SelectorList;
51  import org.htmlunit.cssparser.parser.selector.SelectorSpecificity;
52  import org.htmlunit.cyberneko.util.FastHashMap;
53  import org.htmlunit.html.DefaultElementFactory.OrderedFastHashMapWithLowercaseKeys;
54  import org.htmlunit.javascript.AbstractJavaScriptEngine;
55  import org.htmlunit.javascript.JavaScriptEngine;
56  import org.htmlunit.javascript.host.event.Event;
57  import org.htmlunit.javascript.host.event.EventTarget;
58  import org.htmlunit.javascript.host.event.MouseEvent;
59  import org.htmlunit.javascript.host.event.PointerEvent;
60  import org.htmlunit.util.OrderedFastHashMap;
61  import org.htmlunit.util.StringUtils;
62  import org.w3c.dom.Attr;
63  import org.w3c.dom.DOMException;
64  import org.w3c.dom.Element;
65  import org.w3c.dom.NamedNodeMap;
66  import org.w3c.dom.Node;
67  import org.w3c.dom.TypeInfo;
68  import org.xml.sax.SAXException;
69  
70  /**
71   * @author Ahmed Ashour
72   * @author Marc Guillemot
73   * @author Tom Anderson
74   * @author Ronald Brill
75   * @author Frank Danek
76   * @author Sven Strickroth
77   */
78  public class DomElement extends DomNamespaceNode implements Element {
79  
80      private static final Log LOG = LogFactory.getLog(DomElement.class);
81  
82      /** id. */
83      public static final String ID_ATTRIBUTE = "id";
84  
85      /** name. */
86      public static final String NAME_ATTRIBUTE = "name";
87  
88      /** src. */
89      public static final String SRC_ATTRIBUTE = "src";
90  
91      /** value. */
92      public static final String VALUE_ATTRIBUTE = "value";
93  
94      /** type. */
95      public static final String TYPE_ATTRIBUTE = "type";
96  
97      /** Constant meaning that the specified attribute was not defined. */
98      public static final String ATTRIBUTE_NOT_DEFINED = new String("");
99  
100     /** Constant meaning that the specified attribute was found but its value was empty. */
101     public static final String ATTRIBUTE_VALUE_EMPTY = new String();
102 
103     /** The map holding the attributes, keyed by name. */
104     private NamedAttrNodeMapImpl attributes_;
105 
106     /** The map holding the namespaces, keyed by URI. */
107     private FastHashMap<String, String> namespaces_;
108 
109     /** Cache for the styles. */
110     private String styleString_;
111     private LinkedHashMap<String, StyleElement> styleMap_;
112 
113     private static final Comparator<StyleElement> STYLE_ELEMENT_COMPARATOR =
114             (first, second) -> StyleElement.compareToByImportanceAndSpecificity(first, second);
115 
116     /**
117      * Whether the Mouse is currently over this element or not.
118      */
119     private boolean mouseOver_;
120 
121     /**
122      * Creates an instance of a DOM element that can have a namespace.
123      *
124      * @param namespaceURI the URI that identifies an XML namespace
125      * @param qualifiedName the qualified name of the element type to instantiate
126      * @param page the page that contains this element
127      * @param attributes a map ready initialized with the attributes for this element, or
128      *        {@code null}. The map will be stored as is, not copied.
129      */
130     public DomElement(final String namespaceURI, final String qualifiedName, final SgmlPage page,
131             final Map<String, DomAttr> attributes) {
132         super(namespaceURI, qualifiedName, page);
133 
134         if (attributes == null) {
135             attributes_ = new NamedAttrNodeMapImpl(this, isAttributeCaseSensitive());
136         }
137         else {
138             attributes_ = new NamedAttrNodeMapImpl(this, isAttributeCaseSensitive(), attributes);
139 
140             for (final DomAttr entry : attributes.values()) {
141                 entry.setParentNode(this);
142                 final String attrNamespaceURI = entry.getNamespaceURI();
143                 final String prefix = entry.getPrefix();
144 
145                 if (attrNamespaceURI != null && prefix != null) {
146                     if (namespaces_ == null) {
147                         namespaces_ = new FastHashMap<>(1, 0.5f);
148                     }
149                     namespaces_.put(attrNamespaceURI, prefix);
150                 }
151             }
152         }
153     }
154 
155     /**
156      * {@inheritDoc}
157      */
158     @Override
159     public String getNodeName() {
160         return getQualifiedName();
161     }
162 
163     /**
164      * {@inheritDoc}
165      */
166     @Override
167     public final short getNodeType() {
168         return ELEMENT_NODE;
169     }
170 
171     /**
172      * Returns the tag name of this element.
173      * @return the tag name of this element
174      */
175     @Override
176     public final String getTagName() {
177         return getNodeName();
178     }
179 
180     /**
181      * {@inheritDoc}
182      */
183     @Override
184     public final boolean hasAttributes() {
185         return !attributes_.isEmpty();
186     }
187 
188     /**
189      * Returns whether the attribute specified by name has a value.
190      *
191      * @param attributeName the name of the attribute
192      * @return true if an attribute with the given name is specified on this element or has a
193      *        default value, false otherwise.
194      */
195     @Override
196     public boolean hasAttribute(final String attributeName) {
197         return attributes_.containsKey(attributeName);
198     }
199 
200     /**
201      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
202      *
203      * Replaces the value of the named style attribute. If there is no style attribute with the
204      * specified name, a new one is added. If the specified value is an empty (or all whitespace)
205      * string, this method actually removes the named style attribute.
206      * @param name the attribute name (delimiter-separated, not camel-cased)
207      * @param value the attribute value
208      * @param priority the new priority of the property; <code>"important"</code>or the empty string if none.
209      */
210     public void replaceStyleAttribute(final String name, final String value, final String priority) {
211         if (StringUtils.isBlank(value)) {
212             removeStyleAttribute(name);
213             return;
214         }
215 
216         final Map<String, StyleElement> styleMap = getStyleMap();
217         final StyleElement old = styleMap.get(name);
218         final StyleElement element;
219         if (old == null) {
220             element = new StyleElement(name, value, priority, SelectorSpecificity.FROM_STYLE_ATTRIBUTE);
221         }
222         else {
223             element = new StyleElement(name, value, priority,
224                     SelectorSpecificity.FROM_STYLE_ATTRIBUTE, old.getIndex());
225         }
226         styleMap.put(name, element);
227         writeStyleToElement(styleMap);
228     }
229 
230     /**
231      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
232      *
233      * Removes the specified style attribute, returning the value of the removed attribute.
234      * @param name the attribute name (delimiter-separated, not camel-cased)
235      * @return the removed value
236      */
237     public String removeStyleAttribute(final String name) {
238         final Map<String, StyleElement> styleMap = getStyleMap();
239         final StyleElement value = styleMap.get(name);
240         if (value == null) {
241             return "";
242         }
243         styleMap.remove(name);
244         writeStyleToElement(styleMap);
245         return value.getValue();
246     }
247 
248     /**
249      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
250      *
251      * Determines the StyleElement for the given name.
252      *
253      * @param name the name of the requested StyleElement
254      * @return the StyleElement or null if not found
255      */
256     public StyleElement getStyleElement(final String name) {
257         final Map<String, StyleElement> map = getStyleMap();
258         if (map != null) {
259             return map.get(name);
260         }
261         return null;
262     }
263 
264     /**
265      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
266      *
267      * Determines the StyleElement for the given name.
268      * This ignores the case of the name.
269      *
270      * @param name the name of the requested StyleElement
271      * @return the StyleElement or null if not found
272      */
273     public StyleElement getStyleElementCaseInSensitive(final String name) {
274         final Map<String, StyleElement> map = getStyleMap();
275         for (final Map.Entry<String, StyleElement> entry : map.entrySet()) {
276             if (entry.getKey().equalsIgnoreCase(name)) {
277                 return entry.getValue();
278             }
279         }
280         return null;
281     }
282 
283     /**
284      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
285      *
286      * Returns a sorted map containing style elements, keyed on style element name. We use a
287      * {@link LinkedHashMap} map so that results are deterministic and are thus testable.
288      *
289      * @return a sorted map containing style elements, keyed on style element name
290      */
291     public LinkedHashMap<String, StyleElement> getStyleMap() {
292         final String styleAttribute = getAttributeDirect("style");
293         if (styleString_ == styleAttribute) {
294             return styleMap_;
295         }
296 
297         final LinkedHashMap<String, StyleElement> styleMap = new LinkedHashMap<>();
298         if (ATTRIBUTE_NOT_DEFINED == styleAttribute || ATTRIBUTE_VALUE_EMPTY == styleAttribute) {
299             styleMap_ = styleMap;
300             styleString_ = styleAttribute;
301             return styleMap_;
302         }
303 
304         final CSSStyleDeclarationImpl cssStyle = new CSSStyleDeclarationImpl(null);
305         try {
306             // use the configured cssErrorHandler here to do the same error handling during
307             // parsing of inline styles like for external css
308             cssStyle.setCssText(styleAttribute, getPage().getWebClient().getCssErrorHandler());
309         }
310         catch (final Exception e) {
311             if (LOG.isErrorEnabled()) {
312                 LOG.error("Error while parsing style value '" + styleAttribute + "'", e);
313             }
314         }
315 
316         for (final Property prop : cssStyle.getProperties()) {
317             final String key = prop.getName().toLowerCase(Locale.ROOT);
318             final StyleElement element = new StyleElement(key,
319                     prop.getValue().getCssText(),
320                     prop.isImportant() ? StyleElement.PRIORITY_IMPORTANT : "",
321                     SelectorSpecificity.FROM_STYLE_ATTRIBUTE);
322             styleMap.put(key, element);
323         }
324 
325         styleMap_ = styleMap;
326         styleString_ = styleAttribute;
327         // styleString_ = cssStyle.getCssText();
328         return styleMap_;
329     }
330 
331     /**
332      * Prints the content between "&lt;" and "&gt;" (or "/&gt;") in the output of the tag name
333      * and its attributes in XML format.
334      * @param printWriter the writer to print in
335      */
336     protected void printOpeningTagContentAsXml(final PrintWriter printWriter) {
337         printWriter.print(getTagName());
338         for (final Map.Entry<String, DomAttr> entry : attributes_.entrySet()) {
339             printWriter.print(" ");
340             printWriter.print(entry.getKey());
341             printWriter.print("=\"");
342             printWriter.print(StringUtils.escapeXmlAttributeValue(entry.getValue().getNodeValue()));
343             printWriter.print("\"");
344         }
345     }
346 
347     /**
348      * {@inheritDoc}
349      */
350     @Override
351     protected boolean printXml(final String indent, final boolean tagBefore, final PrintWriter printWriter) {
352         final boolean hasChildren = getFirstChild() != null;
353 
354         if (tagBefore) {
355             printWriter.print("\r\n");
356             printWriter.print(indent);
357         }
358 
359         printWriter.print('<');
360         printOpeningTagContentAsXml(printWriter);
361 
362         if (hasChildren) {
363             printWriter.print(">");
364             final boolean tag = printChildrenAsXml(indent, true, printWriter);
365             if (tag) {
366                 printWriter.print("\r\n");
367                 printWriter.print(indent);
368             }
369             printWriter.print("</");
370             printWriter.print(getTagName());
371             printWriter.print(">");
372         }
373         else if (isEmptyXmlTagExpanded()) {
374             printWriter.print("></");
375             printWriter.print(getTagName());
376             printWriter.print(">");
377         }
378         else {
379             printWriter.print("/>");
380         }
381 
382         return true;
383     }
384 
385     /**
386      * Indicates if a node without children should be written in expanded form as XML
387      * (i.e. with closing tag rather than with "/&gt;")
388      * @return {@code false} by default
389      */
390     protected boolean isEmptyXmlTagExpanded() {
391         return false;
392     }
393 
394     /**
395      * Returns the qualified name (prefix:local) for the specified namespace and local name,
396      * or {@code null} if the specified namespace URI does not exist.
397      *
398      * @param namespaceURI the URI that identifies an XML namespace
399      * @param localName the name within the namespace
400      * @return the qualified name for the specified namespace and local name
401      */
402     String getQualifiedName(final String namespaceURI, final String localName) {
403         final String qualifiedName;
404         if (namespaceURI == null) {
405             qualifiedName = localName;
406         }
407         else {
408             final String prefix = namespaces_ == null ? null : namespaces_.get(namespaceURI);
409             if (prefix == null) {
410                 qualifiedName = null;
411             }
412             else {
413                 qualifiedName = prefix + ':' + localName;
414             }
415         }
416         return qualifiedName;
417     }
418 
419     /**
420      * Returns the value of the attribute specified by name or an empty string. If the
421      * result is an empty string then it will be either {@link #ATTRIBUTE_NOT_DEFINED}
422      * if the attribute wasn't specified or {@link #ATTRIBUTE_VALUE_EMPTY} if the
423      * attribute was specified, but it was empty.
424      *
425      * @param attributeName the name of the attribute
426      * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY}
427      */
428     @Override
429     public String getAttribute(final String attributeName) {
430         final DomAttr attr = attributes_.get(attributeName);
431         if (attr != null) {
432             return attr.getNodeValue();
433         }
434         return ATTRIBUTE_NOT_DEFINED;
435     }
436 
437     /**
438      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
439      *
440      * @param attributeName the name of the attribute
441      * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY}
442      */
443     public String getAttributeDirect(final String attributeName) {
444         final DomAttr attr = attributes_.getDirect(attributeName);
445         if (attr != null) {
446             return attr.getNodeValue();
447         }
448         return ATTRIBUTE_NOT_DEFINED;
449     }
450 
451     /**
452      * Removes an attribute specified by name from this element.
453      * @param attributeName the attribute attributeName
454      */
455     @Override
456     public void removeAttribute(final String attributeName) {
457         attributes_.remove(attributeName);
458     }
459 
460     /**
461      * Removes an attribute specified by namespace and local name from this element.
462      * @param namespaceURI the URI that identifies an XML namespace
463      * @param localName the name within the namespace
464      */
465     @Override
466     public final void removeAttributeNS(final String namespaceURI, final String localName) {
467         final String qualifiedName = getQualifiedName(namespaceURI, localName);
468         if (qualifiedName != null) {
469             removeAttribute(qualifiedName);
470         }
471     }
472 
473     /**
474      * {@inheritDoc}
475      * Not yet implemented.
476      */
477     @Override
478     public final Attr removeAttributeNode(final Attr attribute) {
479         throw new UnsupportedOperationException("DomElement.removeAttributeNode is not yet implemented.");
480     }
481 
482     /**
483      * Returns whether the attribute specified by namespace and local name has a value.
484      *
485      * @param namespaceURI the URI that identifies an XML namespace
486      * @param localName the name within the namespace
487      * @return true if an attribute with the given name is specified on this element or has a
488      *         default value, false otherwise.
489      */
490     @Override
491     public final boolean hasAttributeNS(final String namespaceURI, final String localName) {
492         final String qualifiedName = getQualifiedName(namespaceURI, localName);
493         if (qualifiedName != null) {
494             return attributes_.get(qualifiedName) != null;
495         }
496         return false;
497     }
498 
499     /**
500      * Returns the map holding the attributes, keyed by name.
501      * @return the attributes map
502      */
503     public final Map<String, DomAttr> getAttributesMap() {
504         return attributes_;
505     }
506 
507     /**
508      * {@inheritDoc}
509      */
510     @Override
511     public NamedNodeMap getAttributes() {
512         return attributes_;
513     }
514 
515     /**
516      * Sets the value of the attribute specified by name.
517      *
518      * @param attributeName the name of the attribute
519      * @param attributeValue the value of the attribute
520      */
521     @Override
522     public void setAttribute(final String attributeName, final String attributeValue) {
523         setAttributeNS(null, attributeName, attributeValue);
524     }
525 
526     /**
527      * Sets the value of the attribute specified by namespace and qualified name.
528      *
529      * @param namespaceURI the URI that identifies an XML namespace
530      * @param qualifiedName the qualified name (prefix:local) of the attribute
531      * @param attributeValue the value of the attribute
532      */
533     @Override
534     public void setAttributeNS(final String namespaceURI, final String qualifiedName,
535             final String attributeValue) {
536         setAttributeNS(namespaceURI, qualifiedName, attributeValue, true, true);
537     }
538 
539     /**
540      * Sets the value of the attribute specified by namespace and qualified name.
541      *
542      * @param namespaceURI the URI that identifies an XML namespace
543      * @param qualifiedName the qualified name (prefix:local) of the attribute
544      * @param attributeValue the value of the attribute
545      * @param notifyAttributeChangeListeners to notify the associated {@link HtmlAttributeChangeListener}s
546      * @param notifyMutationObservers to notify {@code MutationObserver}s or not
547      */
548     protected void setAttributeNS(final String namespaceURI, final String qualifiedName,
549             final String attributeValue, final boolean notifyAttributeChangeListeners,
550             final boolean notifyMutationObservers) {
551         final DomAttr newAttr = new DomAttr(getPage(), namespaceURI, qualifiedName, attributeValue, true);
552         newAttr.setParentNode(this);
553         attributes_.put(qualifiedName, newAttr);
554 
555         if (namespaceURI != null) {
556             if (namespaces_ == null) {
557                 namespaces_ = new FastHashMap<>(1, 0.5f);
558             }
559             namespaces_.put(namespaceURI, newAttr.getPrefix());
560         }
561     }
562 
563     /**
564      * Indicates if the attribute names are case sensitive.
565      * @return {@code true}
566      */
567     protected boolean isAttributeCaseSensitive() {
568         return true;
569     }
570 
571     /**
572      * Returns the value of the attribute specified by namespace and local name or an empty
573      * string. If the result is an empty string then it will be either {@link #ATTRIBUTE_NOT_DEFINED}
574      * if the attribute wasn't specified or {@link #ATTRIBUTE_VALUE_EMPTY} if the
575      * attribute was specified, but it was empty.
576      *
577      * @param namespaceURI the URI that identifies an XML namespace
578      * @param localName the name within the namespace
579      * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY}
580      */
581     @Override
582     public final String getAttributeNS(final String namespaceURI, final String localName) {
583         final String qualifiedName = getQualifiedName(namespaceURI, localName);
584         if (qualifiedName != null) {
585             return getAttribute(qualifiedName);
586         }
587         return ATTRIBUTE_NOT_DEFINED;
588     }
589 
590     /**
591      * {@inheritDoc}
592      */
593     @Override
594     public DomAttr getAttributeNode(final String name) {
595         return attributes_.get(name);
596     }
597 
598     /**
599      * {@inheritDoc}
600      */
601     @Override
602     public DomAttr getAttributeNodeNS(final String namespaceURI, final String localName) {
603         final String qualifiedName = getQualifiedName(namespaceURI, localName);
604         if (qualifiedName != null) {
605             return attributes_.get(qualifiedName);
606         }
607         return null;
608     }
609 
610     /**
611      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
612      *
613      * @param styleMap the styles
614      */
615     public void writeStyleToElement(final Map<String, StyleElement> styleMap) {
616         if (styleMap.isEmpty()) {
617             setAttribute("style", "");
618             return;
619         }
620 
621         final StringBuilder builder = new StringBuilder();
622         final List<StyleElement> styleElements = new ArrayList<>(styleMap.values());
623         styleElements.sort(STYLE_ELEMENT_COMPARATOR);
624         for (final StyleElement e : styleElements) {
625             if (builder.length() != 0) {
626                 builder.append(' ');
627             }
628             builder.append(e.getName())
629                 .append(": ")
630                 .append(e.getValue());
631 
632             final String prio = e.getPriority();
633             if (StringUtils.isNotBlank(prio)) {
634                 builder.append(" !").append(prio);
635             }
636             builder.append(';');
637         }
638         setAttribute("style", builder.toString());
639     }
640 
641     /**
642      * {@inheritDoc}
643      */
644     @Override
645     public DomNodeList<HtmlElement> getElementsByTagName(final String tagName) {
646         return getElementsByTagNameImpl(tagName);
647     }
648 
649     /**
650      * This should be {@link #getElementsByTagName(String)}, but is separate because of the type erasure in Java.
651      * @param tagName The name of the tag to match on
652      * @return A list of matching elements.
653      */
654     <E extends HtmlElement> DomNodeList<E> getElementsByTagNameImpl(final String tagName) {
655         return new AbstractDomNodeList<E>(this) {
656             @Override
657             @SuppressWarnings("unchecked")
658             protected List<E> provideElements() {
659                 final List<E> res = new ArrayList<>();
660                 for (final HtmlElement elem : getDomNode().getHtmlElementDescendants()) {
661                     if (elem.getLocalName().equalsIgnoreCase(tagName)) {
662                         res.add((E) elem);
663                     }
664                 }
665                 return res;
666             }
667         };
668     }
669 
670     /**
671      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
672      *
673      * @param <E> the specific HtmlElement type
674      * @param tagName The name of the tag to match on
675      * @return A list of matching elements; this is not a live list
676      */
677     public <E extends HtmlElement> List<E> getStaticElementsByTagName(final String tagName) {
678         final List<E> res = new ArrayList<>();
679         for (final Iterator<HtmlElement> iterator = this.new DescendantHtmlElementsIterator(); iterator.hasNext();) {
680             final HtmlElement elem = iterator.next();
681             if (elem.getLocalName().equalsIgnoreCase(tagName)) {
682                 final String prefix = elem.getPrefix();
683                 if (prefix == null || prefix.isEmpty()) {
684                     res.add((E) elem);
685                 }
686             }
687         }
688         return res;
689     }
690 
691     /**
692      * {@inheritDoc}
693      * Not yet implemented.
694      */
695     @Override
696     public DomNodeList<HtmlElement> getElementsByTagNameNS(final String namespace, final String localName) {
697         throw new UnsupportedOperationException("DomElement.getElementsByTagNameNS is not yet implemented.");
698     }
699 
700     /**
701      * {@inheritDoc}
702      * Not yet implemented.
703      */
704     @Override
705     public TypeInfo getSchemaTypeInfo() {
706         throw new UnsupportedOperationException("DomElement.getSchemaTypeInfo is not yet implemented.");
707     }
708 
709     /**
710      * {@inheritDoc}
711      * Not yet implemented.
712      */
713     @Override
714     public void setIdAttribute(final String name, final boolean isId) {
715         throw new UnsupportedOperationException("DomElement.setIdAttribute is not yet implemented.");
716     }
717 
718     /**
719      * {@inheritDoc}
720      * Not yet implemented.
721      */
722     @Override
723     public void setIdAttributeNS(final String namespaceURI, final String localName, final boolean isId) {
724         throw new UnsupportedOperationException("DomElement.setIdAttributeNS is not yet implemented.");
725     }
726 
727     /**
728      * {@inheritDoc}
729      */
730     @Override
731     public Attr setAttributeNode(final Attr attribute) {
732         attributes_.setNamedItem(attribute);
733         return null;
734     }
735 
736     /**
737      * {@inheritDoc}
738      * Not yet implemented.
739      */
740     @Override
741     public Attr setAttributeNodeNS(final Attr attribute) {
742         throw new UnsupportedOperationException("DomElement.setAttributeNodeNS is not yet implemented.");
743     }
744 
745     /**
746      * {@inheritDoc}
747      * Not yet implemented.
748      */
749     @Override
750     public final void setIdAttributeNode(final Attr idAttr, final boolean isId) {
751         throw new UnsupportedOperationException("DomElement.setIdAttributeNode is not yet implemented.");
752     }
753 
754     /**
755      * {@inheritDoc}
756      */
757     @Override
758     public DomNode cloneNode(final boolean deep) {
759         final DomElement clone = (DomElement) super.cloneNode(deep);
760         clone.attributes_ = new NamedAttrNodeMapImpl(clone, isAttributeCaseSensitive());
761         clone.attributes_.putAll(attributes_);
762         return clone;
763     }
764 
765     /**
766      * @return the identifier of this element
767      */
768     public final String getId() {
769         return getAttributeDirect(ID_ATTRIBUTE);
770     }
771 
772     /**
773      * Sets the identifier this element.
774      *
775      * @param newId the new identifier of this element
776      */
777     public final void setId(final String newId) {
778         setAttribute(ID_ATTRIBUTE, newId);
779     }
780 
781     /**
782      * Returns the first child element node of this element. null if this element has no child elements.
783      * @return the first child element node of this element. null if this element has no child elements
784      */
785     public DomElement getFirstElementChild() {
786         final Iterator<DomElement> i = getChildElements().iterator();
787         if (i.hasNext()) {
788             return i.next();
789         }
790         return null;
791     }
792 
793     /**
794      * Returns the last child element node of this element. null if this element has no child elements.
795      * @return the last child element node of this element. null if this element has no child elements
796      */
797     public DomElement getLastElementChild() {
798         DomElement lastChild = null;
799         for (final DomElement domElement : getChildElements()) {
800             lastChild = domElement;
801         }
802         return lastChild;
803     }
804 
805     /**
806      * Returns the current number of element nodes that are children of this element.
807      * @return the current number of element nodes that are children of this element.
808      */
809     public int getChildElementCount() {
810         int counter = 0;
811 
812         for (final DomElement domElement : getChildElements()) {
813             counter++;
814         }
815         return counter;
816     }
817 
818     /**
819      * @return an Iterable over the DomElement children of this object, i.e. excluding the non-element nodes
820      */
821     public final Iterable<DomElement> getChildElements() {
822         return new ChildElementsIterable(this);
823     }
824 
825     /**
826      * An Iterable over the DomElement children.
827      */
828     private static class ChildElementsIterable implements Iterable<DomElement> {
829         private final Iterator<DomElement> iterator_;
830 
831         /**
832          * Constructor.
833          * @param domNode the parent
834          */
835         protected ChildElementsIterable(final DomNode domNode) {
836             iterator_ = new ChildElementsIterator(domNode);
837         }
838 
839         @Override
840         public Iterator<DomElement> iterator() {
841             return iterator_;
842         }
843     }
844 
845     /**
846      * An iterator over the DomElement children.
847      */
848     protected static class ChildElementsIterator implements Iterator<DomElement> {
849 
850         private DomElement nextElement_;
851 
852         /**
853          * Constructor.
854          * @param domNode the parent
855          */
856         protected ChildElementsIterator(final DomNode domNode) {
857             final DomNode child = domNode.getFirstChild();
858             if (child != null) {
859                 if (child instanceof DomElement) {
860                     nextElement_ = (DomElement) child;
861                 }
862                 else {
863                     setNextElement(child);
864                 }
865             }
866         }
867 
868         /**
869          * @return is there a next one ?
870          */
871         @Override
872         public boolean hasNext() {
873             return nextElement_ != null;
874         }
875 
876         /**
877          * @return the next one
878          */
879         @Override
880         public DomElement next() {
881             if (nextElement_ != null) {
882                 final DomElement result = nextElement_;
883                 setNextElement(nextElement_);
884                 return result;
885             }
886             throw new NoSuchElementException();
887         }
888 
889         /** Removes the current one. */
890         @Override
891         public void remove() {
892             if (nextElement_ == null) {
893                 throw new IllegalStateException();
894             }
895             final DomNode sibling = nextElement_.getPreviousSibling();
896             if (sibling != null) {
897                 sibling.remove();
898             }
899         }
900 
901         private void setNextElement(final DomNode node) {
902             DomNode next = node.getNextSibling();
903             while (next != null && !(next instanceof DomElement)) {
904                 next = next.getNextSibling();
905             }
906             nextElement_ = (DomElement) next;
907         }
908     }
909 
910     /**
911      * Returns a string representation of this element.
912      * @return a string representation of this element
913      */
914     @Override
915     public String toString() {
916         final StringWriter writer = new StringWriter();
917         final PrintWriter printWriter = new PrintWriter(writer);
918 
919         printWriter.print(getClass().getSimpleName());
920         printWriter.print("[<");
921         printOpeningTagContentAsXml(printWriter);
922         printWriter.print(">]");
923         printWriter.flush();
924         return writer.toString();
925     }
926 
927     /**
928      * Simulates clicking on this element, returning the page in the window that has the focus
929      * after the element has been clicked. Note that the returned page may or may not be the same
930      * as the original page, depending on the type of element being clicked, the presence of JavaScript
931      * action listeners, etc.<br>
932      * This only clicks the element if it is visible and enabled (isDisplayed() &amp; !isDisabled()).
933      * In case the element is not visible and/or disabled, only a log output is generated.
934      * <br>
935      * If you circumvent the visible/disabled check use click(shiftKey, ctrlKey, altKey, true, true, false)
936      *
937      * @param <P> the page type
938      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
939      * @exception IOException if an IO error occurs
940      */
941     public <P extends Page> P click() throws IOException {
942         return click(false, false, false);
943     }
944 
945     /**
946      * Simulates clicking on this element, returning the page in the window that has the focus
947      * after the element has been clicked. Note that the returned page may or may not be the same
948      * as the original page, depending on the type of element being clicked, the presence of JavaScript
949      * action listeners, etc.<br>
950      * This only clicks the element if it is visible and enabled (isDisplayed() &amp; !isDisabled()).
951      * In case the element is not visible and/or disabled, only a log output is generated.
952      * <br>
953      * If you circumvent the visible/disabled check use click(shiftKey, ctrlKey, altKey, true, true, false)
954      *
955      * @param shiftKey {@code true} if SHIFT is pressed during the click
956      * @param ctrlKey {@code true} if CTRL is pressed during the click
957      * @param altKey {@code true} if ALT is pressed during the click
958      * @param <P> the page type
959      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
960      * @exception IOException if an IO error occurs
961      */
962     public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey)
963         throws IOException {
964 
965         return click(shiftKey, ctrlKey, altKey, true);
966     }
967 
968     /**
969      * Simulates clicking on this element, returning the page in the window that has the focus
970      * after the element has been clicked. Note that the returned page may or may not be the same
971      * as the original page, depending on the type of element being clicked, the presence of JavaScript
972      * action listeners, etc.<br>
973      * This only clicks the element if it is visible and enabled (isDisplayed() &amp; !isDisabled()).
974      * In case the element is not visible and/or disabled, only a log output is generated.
975      * <br>
976      * If you circumvent the visible/disabled check use click(shiftKey, ctrlKey, altKey, true, true, false)
977      *
978      * @param shiftKey {@code true} if SHIFT is pressed during the click
979      * @param ctrlKey {@code true} if CTRL is pressed during the click
980      * @param altKey {@code true} if ALT is pressed during the click
981      * @param triggerMouseEvents if true trigger the mouse events also
982      * @param <P> the page type
983      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
984      * @exception IOException if an IO error occurs
985      */
986     public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
987             final boolean triggerMouseEvents) throws IOException {
988         return click(shiftKey, ctrlKey, altKey, triggerMouseEvents, true, false, false);
989     }
990 
991     /**
992      * @return true if this is an {@link DisabledElement} and disabled
993      */
994     protected boolean isDisabledElementAndDisabled() {
995         return this instanceof DisabledElement && ((DisabledElement) this).isDisabled();
996     }
997 
998     /**
999      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1000      *
1001      * Simulates clicking on this element, returning the page in the window that has the focus
1002      * after the element has been clicked. Note that the returned page may or may not be the same
1003      * as the original page, depending on the type of element being clicked, the presence of JavaScript
1004      * action listeners, etc.
1005      *
1006      * @param shiftKey {@code true} if SHIFT is pressed during the click
1007      * @param ctrlKey {@code true} if CTRL is pressed during the click
1008      * @param altKey {@code true} if ALT is pressed during the click
1009      * @param triggerMouseEvents if true trigger the mouse events also
1010      * @param handleFocus if true set the focus (and trigger the event)
1011      * @param ignoreVisibility whether to ignore visibility or not
1012      * @param disableProcessLabelAfterBubbling ignore label processing
1013      * @param <P> the page type
1014      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
1015      * @exception IOException if an IO error occurs
1016      */
1017     @SuppressWarnings("unchecked")
1018     public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
1019             final boolean triggerMouseEvents, final boolean handleFocus, final boolean ignoreVisibility,
1020             final boolean disableProcessLabelAfterBubbling) throws IOException {
1021 
1022         // make enclosing window the current one
1023         final SgmlPage page = getPage();
1024         final WebClient webClient = page.getWebClient();
1025         webClient.setCurrentWindow(page.getEnclosingWindow());
1026 
1027         if (!ignoreVisibility) {
1028             if (!(page instanceof HtmlPage)) {
1029                 return (P) page;
1030             }
1031 
1032             if (!isDisplayed()) {
1033                 if (LOG.isWarnEnabled()) {
1034                     LOG.warn("Calling click() ignored because the target element '" + this
1035                                     + "' is not displayed.");
1036                 }
1037                 return (P) page;
1038             }
1039 
1040             if (isDisabledElementAndDisabled()) {
1041                 if (LOG.isWarnEnabled()) {
1042                     LOG.warn("Calling click() ignored because the target element '" + this + "' is disabled.");
1043                 }
1044                 return (P) page;
1045             }
1046         }
1047 
1048         synchronized (page) {
1049             if (triggerMouseEvents) {
1050                 mouseDown(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT);
1051             }
1052 
1053             final AbstractJavaScriptEngine<?> jsEngine = webClient.getJavaScriptEngine();
1054             if (webClient.isJavaScriptEnabled()) {
1055                 jsEngine.holdPosponedActions();
1056             }
1057             try {
1058                 if (handleFocus) {
1059                     // give focus to current element (if possible) or only remove it from previous one
1060                     DomElement elementToFocus = null;
1061                     if (this instanceof SubmittableElement
1062                         || this instanceof HtmlAnchor
1063                             && ATTRIBUTE_NOT_DEFINED != ((HtmlAnchor) this).getHrefAttribute()
1064                         || this instanceof HtmlArea
1065                             && (ATTRIBUTE_NOT_DEFINED != ((HtmlArea) this).getHrefAttribute()
1066                                 || webClient.getBrowserVersion().hasFeature(JS_AREA_WITHOUT_HREF_FOCUSABLE))
1067                         || this instanceof HtmlElement && ((HtmlElement) this).getTabIndex() != null) {
1068                         elementToFocus = this;
1069                     }
1070                     else if (this instanceof HtmlOption) {
1071                         elementToFocus = ((HtmlOption) this).getEnclosingSelect();
1072                     }
1073 
1074                     if (elementToFocus == null) {
1075                         ((HtmlPage) page).setFocusedElement(null);
1076                     }
1077                     else {
1078                         elementToFocus.focus();
1079                     }
1080                 }
1081 
1082                 if (triggerMouseEvents) {
1083                     mouseUp(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT);
1084                 }
1085 
1086                 MouseEvent event = null;
1087                 if (webClient.isJavaScriptEnabled()) {
1088                     final BrowserVersion browser = webClient.getBrowserVersion();
1089                     if (browser.hasFeature(EVENT_ONCLICK_USES_POINTEREVENT)) {
1090                         event = new PointerEvent(getEventTargetElement(), MouseEvent.TYPE_CLICK, shiftKey,
1091                                 ctrlKey, altKey, MouseEvent.BUTTON_LEFT, 1);
1092                     }
1093                     else {
1094                         event = new MouseEvent(getEventTargetElement(), MouseEvent.TYPE_CLICK, shiftKey,
1095                                 ctrlKey, altKey, MouseEvent.BUTTON_LEFT, 1);
1096                     }
1097 
1098                     if (disableProcessLabelAfterBubbling) {
1099                         event.disableProcessLabelAfterBubbling();
1100                     }
1101                 }
1102                 click(event, shiftKey, ctrlKey, altKey, ignoreVisibility);
1103             }
1104             finally {
1105                 if (webClient.isJavaScriptEnabled()) {
1106                     jsEngine.processPostponedActions();
1107                 }
1108             }
1109 
1110             return (P) webClient.getCurrentWindow().getEnclosedPage();
1111         }
1112     }
1113 
1114     /**
1115      * Returns the event target element. This could be overridden by subclasses to have other targets.
1116      * The default implementation returns 'this'.
1117      * @return the event target element.
1118      */
1119     protected DomNode getEventTargetElement() {
1120         return this;
1121     }
1122 
1123     /**
1124      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1125      *
1126      * Simulates clicking on this element, returning the page in the window that has the focus
1127      * after the element has been clicked. Note that the returned page may or may not be the same
1128      * as the original page, depending on the type of element being clicked, the presence of JavaScript
1129      * action listeners, etc.
1130      *
1131      * @param event the click event used
1132      * @param shiftKey {@code true} if SHIFT is pressed during the click
1133      * @param ctrlKey {@code true} if CTRL is pressed during the click
1134      * @param altKey {@code true} if ALT is pressed during the click
1135      * @param ignoreVisibility whether to ignore visibility or not
1136      * @param <P> the page type
1137      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
1138      * @exception IOException if an IO error occurs
1139      */
1140     @SuppressWarnings("unchecked")
1141     public <P extends Page> P click(final Event event,
1142                         final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
1143                         final boolean ignoreVisibility) throws IOException {
1144         final SgmlPage page = getPage();
1145 
1146         if ((!ignoreVisibility && !isDisplayed()) || isDisabledElementAndDisabled()) {
1147             return (P) page;
1148         }
1149 
1150         final WebClient webClient = page.getWebClient();
1151         if (!webClient.isJavaScriptEnabled()) {
1152             doClickStateUpdate(shiftKey, ctrlKey);
1153 
1154             webClient.loadDownloadedResponses();
1155             return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
1156         }
1157 
1158         // may be different from page when working with "orphaned pages"
1159         // (ex: clicking a link in a page that is not active anymore)
1160         final Page contentPage = page.getEnclosingWindow().getEnclosedPage();
1161 
1162         boolean stateUpdated = false;
1163         boolean changed = false;
1164         if (isStateUpdateFirst()) {
1165             changed = doClickStateUpdate(shiftKey, ctrlKey);
1166             stateUpdated = true;
1167         }
1168 
1169         final ScriptResult scriptResult = doClickFireClickEvent(event);
1170         final boolean eventIsAborted = event.isAborted(scriptResult);
1171 
1172         final boolean pageAlreadyChanged = contentPage != page.getEnclosingWindow().getEnclosedPage();
1173         if (!pageAlreadyChanged && !stateUpdated && !eventIsAborted) {
1174             changed = doClickStateUpdate(shiftKey, ctrlKey);
1175         }
1176 
1177         if (changed) {
1178             doClickFireChangeEvent();
1179         }
1180 
1181         webClient.loadDownloadedResponses();
1182         return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
1183     }
1184 
1185     /**
1186      * This method implements the control state update part of the click action.
1187      *
1188      * <p>The default implementation only calls doClickStateUpdate on parent's DomElement (if any).
1189      * Subclasses requiring different behavior (like {@link HtmlSubmitInput}) will override this method.</p>
1190      * @param shiftKey {@code true} if SHIFT is pressed
1191      * @param ctrlKey {@code true} if CTRL is pressed
1192      *
1193      * @return true if doClickFireEvent method has to be called later on (to signal,
1194      *         that the value was changed)
1195      * @throws IOException if an IO error occurs
1196      */
1197     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
1198         if (propagateClickStateUpdateToParent()) {
1199             // needed for instance to perform link doClickAction when a nested element is clicked
1200             // it should probably be changed to do this at the event level but currently
1201             // this wouldn't work with JS disabled as events are propagated in the host object tree.
1202             final DomNode parent = getParentNode();
1203             if (parent instanceof DomElement) {
1204                 return ((DomElement) parent).doClickStateUpdate(false, false);
1205             }
1206         }
1207 
1208         return false;
1209     }
1210 
1211     /**
1212      * Usually the click is propagated to the parent. Overwrite if you like to disable this.
1213      * @return true or false
1214      * @see #doClickStateUpdate(boolean, boolean)
1215      */
1216     protected boolean propagateClickStateUpdateToParent() {
1217         return true;
1218     }
1219 
1220     /**
1221      * This method implements the control onchange handler call during the click action.
1222      */
1223     protected void doClickFireChangeEvent() {
1224         // nothing to do, in the default case
1225     }
1226 
1227     /**
1228      * This method implements the control onclick handler call during the click action.
1229      * @param event the click event used
1230      * @return the script result
1231      */
1232     protected ScriptResult doClickFireClickEvent(final Event event) {
1233         return fireEvent(event);
1234     }
1235 
1236     /**
1237      * Simulates double-clicking on this element, returning the page in the window that has the focus
1238      * after the element has been clicked. Note that the returned page may or may not be the same
1239      * as the original page, depending on the type of element being clicked, the presence of JavaScript
1240      * action listeners, etc. Note also that {@link #click()} is automatically called first.
1241      *
1242      * @param <P> the page type
1243      * @return the page that occupies this element's window after the element has been double-clicked
1244      * @exception IOException if an IO error occurs
1245      */
1246     public <P extends Page> P dblClick() throws IOException {
1247         return dblClick(false, false, false);
1248     }
1249 
1250     /**
1251      * Simulates double-clicking on this element, returning the page in the window that has the focus
1252      * after the element has been clicked. Note that the returned page may or may not be the same
1253      * as the original page, depending on the type of element being clicked, the presence of JavaScript
1254      * action listeners, etc. Note also that {@link #click(boolean, boolean, boolean)} is automatically
1255      * called first.
1256      *
1257      * @param shiftKey {@code true} if SHIFT is pressed during the double click
1258      * @param ctrlKey {@code true} if CTRL is pressed during the double click
1259      * @param altKey {@code true} if ALT is pressed during the double click
1260      * @param <P> the page type
1261      * @return the page that occupies this element's window after the element has been double-clicked
1262      * @exception IOException if an IO error occurs
1263      */
1264     @SuppressWarnings("unchecked")
1265     public <P extends Page> P dblClick(final boolean shiftKey, final boolean ctrlKey, final boolean altKey)
1266         throws IOException {
1267         if (isDisabledElementAndDisabled()) {
1268             return (P) getPage();
1269         }
1270 
1271         // call click event first
1272         P clickPage = click(shiftKey, ctrlKey, altKey);
1273         if (clickPage != getPage()) {
1274             LOG.debug("dblClick() is ignored, as click() loaded a different page.");
1275             return clickPage;
1276         }
1277 
1278         // call click event a second time
1279         clickPage = click(shiftKey, ctrlKey, altKey);
1280         if (clickPage != getPage()) {
1281             LOG.debug("dblClick() is ignored, as click() loaded a different page.");
1282             return clickPage;
1283         }
1284 
1285         final Event event;
1286         event = new MouseEvent(this, MouseEvent.TYPE_DBL_CLICK, shiftKey, ctrlKey, altKey,
1287                 MouseEvent.BUTTON_LEFT, 2);
1288 
1289         final ScriptResult scriptResult = fireEvent(event);
1290         if (scriptResult == null) {
1291             return clickPage;
1292         }
1293         return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
1294     }
1295 
1296     /**
1297      * Simulates moving the mouse over this element, returning the page which this element's window contains
1298      * after the mouse move. The returned page may or may not be the same as the original page, depending
1299      * on JavaScript event handlers, etc.
1300      *
1301      * @return the page which this element's window contains after the mouse move
1302      */
1303     public Page mouseOver() {
1304         return mouseOver(false, false, false, MouseEvent.BUTTON_LEFT);
1305     }
1306 
1307     /**
1308      * Simulates moving the mouse over this element, returning the page which this element's window contains
1309      * after the mouse move. The returned page may or may not be the same as the original page, depending
1310      * on JavaScript event handlers, etc.
1311      *
1312      * @param shiftKey {@code true} if SHIFT is pressed during the mouse move
1313      * @param ctrlKey {@code true} if CTRL is pressed during the mouse move
1314      * @param altKey {@code true} if ALT is pressed during the mouse move
1315      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1316      *        or {@link MouseEvent#BUTTON_RIGHT}
1317      * @return the page which this element's window contains after the mouse move
1318      */
1319     public Page mouseOver(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1320         return doMouseEvent(MouseEvent.TYPE_MOUSE_OVER, shiftKey, ctrlKey, altKey, button);
1321     }
1322 
1323     /**
1324      * Simulates moving the mouse over this element, returning the page which this element's window contains
1325      * after the mouse move. The returned page may or may not be the same as the original page, depending
1326      * on JavaScript event handlers, etc.
1327      *
1328      * @return the page which this element's window contains after the mouse move
1329      */
1330     public Page mouseMove() {
1331         return mouseMove(false, false, false, MouseEvent.BUTTON_LEFT);
1332     }
1333 
1334     /**
1335      * Simulates moving the mouse over this element, returning the page which this element's window contains
1336      * after the mouse move. The returned page may or may not be the same as the original page, depending
1337      * on JavaScript event handlers, etc.
1338      *
1339      * @param shiftKey {@code true} if SHIFT is pressed during the mouse move
1340      * @param ctrlKey {@code true} if CTRL is pressed during the mouse move
1341      * @param altKey {@code true} if ALT is pressed during the mouse move
1342      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1343      *        or {@link MouseEvent#BUTTON_RIGHT}
1344      * @return the page which this element's window contains after the mouse move
1345      */
1346     public Page mouseMove(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1347         return doMouseEvent(MouseEvent.TYPE_MOUSE_MOVE, shiftKey, ctrlKey, altKey, button);
1348     }
1349 
1350     /**
1351      * Simulates moving the mouse out of this element, returning the page which this element's window contains
1352      * after the mouse move. The returned page may or may not be the same as the original page, depending
1353      * on JavaScript event handlers, etc.
1354      *
1355      * @return the page which this element's window contains after the mouse move
1356      */
1357     public Page mouseOut() {
1358         return mouseOut(false, false, false, MouseEvent.BUTTON_LEFT);
1359     }
1360 
1361     /**
1362      * Simulates moving the mouse out of this element, returning the page which this element's window contains
1363      * after the mouse move. The returned page may or may not be the same as the original page, depending
1364      * on JavaScript event handlers, etc.
1365      *
1366      * @param shiftKey {@code true} if SHIFT is pressed during the mouse move
1367      * @param ctrlKey {@code true} if CTRL is pressed during the mouse move
1368      * @param altKey {@code true} if ALT is pressed during the mouse move
1369      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1370      *        or {@link MouseEvent#BUTTON_RIGHT}
1371      * @return the page which this element's window contains after the mouse move
1372      */
1373     public Page mouseOut(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1374         return doMouseEvent(MouseEvent.TYPE_MOUSE_OUT, shiftKey, ctrlKey, altKey, button);
1375     }
1376 
1377     /**
1378      * Simulates clicking the mouse on this element, returning the page which this element's window contains
1379      * after the mouse click. The returned page may or may not be the same as the original page, depending
1380      * on JavaScript event handlers, etc.
1381      *
1382      * @return the page which this element's window contains after the mouse click
1383      */
1384     public Page mouseDown() {
1385         return mouseDown(false, false, false, MouseEvent.BUTTON_LEFT);
1386     }
1387 
1388     /**
1389      * Simulates clicking the mouse on this element, returning the page which this element's window contains
1390      * after the mouse click. The returned page may or may not be the same as the original page, depending
1391      * on JavaScript event handlers, etc.
1392      *
1393      * @param shiftKey {@code true} if SHIFT is pressed during the mouse click
1394      * @param ctrlKey {@code true} if CTRL is pressed during the mouse click
1395      * @param altKey {@code true} if ALT is pressed during the mouse click
1396      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1397      *        or {@link MouseEvent#BUTTON_RIGHT}
1398      * @return the page which this element's window contains after the mouse click
1399      */
1400     public Page mouseDown(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1401         return doMouseEvent(MouseEvent.TYPE_MOUSE_DOWN, shiftKey, ctrlKey, altKey, button);
1402     }
1403 
1404     /**
1405      * Simulates releasing the mouse click on this element, returning the page which this element's window contains
1406      * after the mouse click release. The returned page may or may not be the same as the original page, depending
1407      * on JavaScript event handlers, etc.
1408      *
1409      * @return the page which this element's window contains after the mouse click release
1410      */
1411     public Page mouseUp() {
1412         return mouseUp(false, false, false, MouseEvent.BUTTON_LEFT);
1413     }
1414 
1415     /**
1416      * Simulates releasing the mouse click on this element, returning the page which this element's window contains
1417      * after the mouse click release. The returned page may or may not be the same as the original page, depending
1418      * on JavaScript event handlers, etc.
1419      *
1420      * @param shiftKey {@code true} if SHIFT is pressed during the mouse click release
1421      * @param ctrlKey {@code true} if CTRL is pressed during the mouse click release
1422      * @param altKey {@code true} if ALT is pressed during the mouse click release
1423      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1424      *        or {@link MouseEvent#BUTTON_RIGHT}
1425      * @return the page which this element's window contains after the mouse click release
1426      */
1427     public Page mouseUp(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1428         return doMouseEvent(MouseEvent.TYPE_MOUSE_UP, shiftKey, ctrlKey, altKey, button);
1429     }
1430 
1431     /**
1432      * Simulates right clicking the mouse on this element, returning the page which this element's window
1433      * contains after the mouse click. The returned page may or may not be the same as the original page,
1434      * depending on JavaScript event handlers, etc.
1435      *
1436      * @return the page which this element's window contains after the mouse click
1437      */
1438     public Page rightClick() {
1439         return rightClick(false, false, false);
1440     }
1441 
1442     /**
1443      * Simulates right clicking the mouse on this element, returning the page which this element's window
1444      * contains after the mouse click. The returned page may or may not be the same as the original page,
1445      * depending on JavaScript event handlers, etc.
1446      *
1447      * @param shiftKey {@code true} if SHIFT is pressed during the mouse click
1448      * @param ctrlKey {@code true} if CTRL is pressed during the mouse click
1449      * @param altKey {@code true} if ALT is pressed during the mouse click
1450      * @return the page which this element's window contains after the mouse click
1451      */
1452     public Page rightClick(final boolean shiftKey, final boolean ctrlKey, final boolean altKey) {
1453         final Page mouseDownPage = mouseDown(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1454         if (mouseDownPage != getPage()) {
1455             LOG.debug("rightClick() is incomplete, as mouseDown() loaded a different page.");
1456             return mouseDownPage;
1457         }
1458 
1459         final Page mouseUpPage = mouseUp(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1460         if (mouseUpPage != getPage()) {
1461             LOG.debug("rightClick() is incomplete, as mouseUp() loaded a different page.");
1462             return mouseUpPage;
1463         }
1464 
1465         return doMouseEvent(MouseEvent.TYPE_CONTEXT_MENU, shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1466     }
1467 
1468     /**
1469      * Simulates the specified mouse event, returning the page which this element's window contains after the event.
1470      * The returned page may or may not be the same as the original page, depending on JavaScript event handlers, etc.
1471      *
1472      * @param eventType the mouse event type to simulate
1473      * @param shiftKey {@code true} if SHIFT is pressed during the mouse event
1474      * @param ctrlKey {@code true} if CTRL is pressed during the mouse event
1475      * @param altKey {@code true} if ALT is pressed during the mouse event
1476      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1477      *        or {@link MouseEvent#BUTTON_RIGHT}
1478      * @return the page which this element's window contains after the event
1479      */
1480     private Page doMouseEvent(final String eventType, final boolean shiftKey, final boolean ctrlKey,
1481         final boolean altKey, final int button) {
1482         final SgmlPage page = getPage();
1483         final WebClient webClient = getPage().getWebClient();
1484         if (!webClient.isJavaScriptEnabled()) {
1485             return page;
1486         }
1487 
1488         final ScriptResult scriptResult;
1489         final Event event;
1490         if (MouseEvent.TYPE_CONTEXT_MENU.equals(eventType)) {
1491             final BrowserVersion browserVersion = webClient.getBrowserVersion();
1492             if (browserVersion.hasFeature(EVENT_ONCLICK_USES_POINTEREVENT)) {
1493                 if (browserVersion.hasFeature(EVENT_CONTEXT_MENU_HAS_DETAIL_1)) {
1494                     event = new PointerEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 1);
1495                 }
1496                 else {
1497                     event = new PointerEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 0);
1498                 }
1499             }
1500             else {
1501                 event = new MouseEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 1);
1502             }
1503         }
1504         else if (MouseEvent.TYPE_DBL_CLICK.equals(eventType)) {
1505             event = new MouseEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 2);
1506         }
1507         else {
1508             event = new MouseEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 1);
1509         }
1510         scriptResult = fireEvent(event);
1511 
1512         final Page currentPage;
1513         if (scriptResult == null) {
1514             currentPage = page;
1515         }
1516         else {
1517             currentPage = webClient.getCurrentWindow().getEnclosedPage();
1518         }
1519 
1520         final boolean mouseOver = !MouseEvent.TYPE_MOUSE_OUT.equals(eventType);
1521         if (mouseOver_ != mouseOver) {
1522             mouseOver_ = mouseOver;
1523 
1524             page.clearComputedStyles();
1525         }
1526 
1527         return currentPage;
1528     }
1529 
1530     /**
1531      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1532      *
1533      * Shortcut for {@link #fireEvent(Event)}.
1534      * @param eventType the event type (like "load", "click")
1535      * @return the execution result, or {@code null} if nothing is executed
1536      */
1537     public ScriptResult fireEvent(final String eventType) {
1538         if (getPage().getWebClient().isJavaScriptEnabled()) {
1539             return fireEvent(new Event(this, eventType));
1540         }
1541         return null;
1542     }
1543 
1544     /**
1545      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1546      *
1547      * Fires the event on the element. Nothing is done if JavaScript is disabled.
1548      * @param event the event to fire
1549      * @return the execution result, or {@code null} if nothing is executed
1550      */
1551     public ScriptResult fireEvent(final Event event) {
1552         final WebClient client = getPage().getWebClient();
1553         if (!client.isJavaScriptEnabled()) {
1554             return null;
1555         }
1556 
1557         if (!handles(event)) {
1558             return null;
1559         }
1560 
1561         if (LOG.isDebugEnabled()) {
1562             LOG.debug("Firing " + event);
1563         }
1564 
1565         final EventTarget jsElt = getScriptableObject();
1566         final ScriptResult result = ((JavaScriptEngine) client.getJavaScriptEngine())
1567                                         .callSecured(cx -> jsElt.fireEvent(event), getHtmlPageOrNull());
1568         if (event.isAborted(result)) {
1569             preventDefault();
1570         }
1571         return result;
1572     }
1573 
1574     /**
1575      * This method is called if the current fired event is canceled by <code>preventDefault()</code>.
1576      *
1577      * <p>The default implementation does nothing.</p>
1578      */
1579     protected void preventDefault() {
1580         // Empty by default; override as needed.
1581     }
1582 
1583     /**
1584      * Sets the focus on this element.
1585      */
1586     public void focus() {
1587         if (!(this instanceof SubmittableElement
1588             || this instanceof HtmlAnchor && ATTRIBUTE_NOT_DEFINED != ((HtmlAnchor) this).getHrefAttribute()
1589             || this instanceof HtmlArea
1590                 && (ATTRIBUTE_NOT_DEFINED != ((HtmlArea) this).getHrefAttribute()
1591                     || getPage().getWebClient().getBrowserVersion().hasFeature(JS_AREA_WITHOUT_HREF_FOCUSABLE))
1592             || this instanceof HtmlElement && ((HtmlElement) this).getTabIndex() != null)) {
1593             return;
1594         }
1595 
1596         if (!isDisplayed() || isDisabledElementAndDisabled()) {
1597             return;
1598         }
1599 
1600         final HtmlPage page = (HtmlPage) getPage();
1601         page.setFocusedElement(this);
1602     }
1603 
1604     /**
1605      * Removes focus from this element.
1606      */
1607     public void blur() {
1608         final HtmlPage page = (HtmlPage) getPage();
1609         if (page.getFocusedElement() != this) {
1610             return;
1611         }
1612 
1613         page.setFocusedElement(null);
1614     }
1615 
1616     /**
1617      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1618      *
1619      * Gets notified that it has lost the focus.
1620      */
1621     public void removeFocus() {
1622         // nothing
1623     }
1624 
1625     /**
1626      * Returns {@code true} if state updates should be done before onclick event handling. This method
1627      * returns {@code false} by default, and is expected to be overridden to return {@code true} by
1628      * derived classes like {@link HtmlCheckBoxInput}.
1629      * @return {@code true} if state updates should be done before onclick event handling
1630      */
1631     protected boolean isStateUpdateFirst() {
1632         return false;
1633     }
1634 
1635     /**
1636      * Returns whether the Mouse is currently over this element or not.
1637      * @return whether the Mouse is currently over this element or not
1638      */
1639     public boolean isMouseOver() {
1640         if (mouseOver_) {
1641             return true;
1642         }
1643         for (final DomElement child : getChildElements()) {
1644             if (child.isMouseOver()) {
1645                 return true;
1646             }
1647         }
1648         return false;
1649     }
1650 
1651     /**
1652      * Returns true if the element would be selected by the specified selector string; otherwise, returns false.
1653      * @param selectorString the selector to test
1654      * @return true if the element would be selected by the specified selector string; otherwise, returns false.
1655      */
1656     public boolean matches(final String selectorString) {
1657         try {
1658             final WebClient webClient = getPage().getWebClient();
1659             final SelectorList selectorList = getSelectorList(selectorString, webClient);
1660 
1661             if (selectorList != null) {
1662                 for (final Selector selector : selectorList) {
1663                     if (CssStyleSheet.selects(webClient.getBrowserVersion(), selector, this, null, true, true)) {
1664                         return true;
1665                     }
1666                 }
1667             }
1668             return false;
1669         }
1670         catch (final IOException e) {
1671             throw new CSSException("Error parsing CSS selectors from '" + selectorString + "': " + e.getMessage(), e);
1672         }
1673     }
1674 
1675     /**
1676      * {@inheritDoc}
1677      */
1678     @Override
1679     public void setNodeValue(final String value) {
1680         // Default behavior is to do nothing, overridden in some subclasses
1681     }
1682 
1683     /**
1684      * Callback method which allows different HTML element types to perform custom
1685      * initialization of computed styles. For example, body elements in most browsers
1686      * have default values for their margins.
1687      *
1688      * @param style the style to initialize
1689      */
1690     public void setDefaults(final ComputedCssStyleDeclaration style) {
1691         // Empty by default; override as necessary.
1692     }
1693 
1694     /**
1695      * Replaces all child elements of this element with the supplied value parsed as html.
1696      * @param source the new value for the contents of this element
1697      * @throws SAXException in case of error
1698      * @throws IOException in case of error
1699      */
1700     public void setInnerHtml(final String source) throws SAXException, IOException {
1701         removeAllChildren();
1702         getPage().clearComputedStylesUpToRoot(this);
1703 
1704         if (source != null) {
1705             parseHtmlSnippet(source);
1706         }
1707     }
1708 }
1709 
1710 /**
1711  * The {@link NamedNodeMap} to store the node attributes.
1712  */
1713 class NamedAttrNodeMapImpl implements Map<String, DomAttr>, NamedNodeMap, Serializable {
1714     private final OrderedFastHashMap<String, DomAttr> map_;
1715     private final DomElement domNode_;
1716     private final boolean caseSensitive_;
1717 
1718     NamedAttrNodeMapImpl(final DomElement domNode, final boolean caseSensitive) {
1719         super();
1720         if (domNode == null) {
1721             throw new IllegalArgumentException("Provided domNode can't be null.");
1722         }
1723         domNode_ = domNode;
1724         caseSensitive_ = caseSensitive;
1725         map_ = new OrderedFastHashMap<>(0);
1726     }
1727 
1728     NamedAttrNodeMapImpl(final DomElement domNode, final boolean caseSensitive,
1729             final Map<String, DomAttr> attributes) {
1730         super();
1731         if (domNode == null) {
1732             throw new IllegalArgumentException("Provided domNode can't be null.");
1733         }
1734         domNode_ = domNode;
1735         caseSensitive_ = caseSensitive;
1736 
1737         if (attributes instanceof OrderedFastHashMapWithLowercaseKeys) {
1738             // no need to rework the map at all, we are case sensitive, so
1739             // we keep all attributes, and we got the right map from outside too
1740             map_ = (OrderedFastHashMap) attributes;
1741         }
1742         else if (caseSensitive && attributes instanceof OrderedFastHashMap) {
1743             // no need to rework the map at all, we are case sensitive, so
1744             // we keep all attributes, and we got the right map from outside too
1745             map_ = (OrderedFastHashMap) attributes;
1746         }
1747         else {
1748             // this is more expensive but atypical, so we don't have to care that much
1749             map_ = new OrderedFastHashMap<>(attributes.size());
1750             // this will create a new map with all case lowercased and
1751             putAll(attributes);
1752         }
1753     }
1754 
1755     /**
1756      * {@inheritDoc}
1757      */
1758     @Override
1759     public int getLength() {
1760         return size();
1761     }
1762 
1763     /**
1764      * {@inheritDoc}
1765      */
1766     @Override
1767     public DomAttr getNamedItem(final String name) {
1768         return get(name);
1769     }
1770 
1771     private String fixName(final String name) {
1772         if (caseSensitive_) {
1773             return name;
1774         }
1775         return StringUtils.toRootLowerCase(name);
1776     }
1777 
1778     /**
1779      * {@inheritDoc}
1780      */
1781     @Override
1782     public Node getNamedItemNS(final String namespaceURI, final String localName) {
1783         if (domNode_ == null) {
1784             return null;
1785         }
1786         return get(domNode_.getQualifiedName(namespaceURI, fixName(localName)));
1787     }
1788 
1789     /**
1790      * {@inheritDoc}
1791      */
1792     @Override
1793     public Node item(final int index) {
1794         if (index < 0 || index >= map_.size()) {
1795             return null;
1796         }
1797         return map_.getValue(index);
1798     }
1799 
1800     /**
1801      * {@inheritDoc}
1802      */
1803     @Override
1804     public Node removeNamedItem(final String name) throws DOMException {
1805         return remove(name);
1806     }
1807 
1808     /**
1809      * {@inheritDoc}
1810      */
1811     @Override
1812     public Node removeNamedItemNS(final String namespaceURI, final String localName) {
1813         if (domNode_ == null) {
1814             return null;
1815         }
1816         return remove(domNode_.getQualifiedName(namespaceURI, fixName(localName)));
1817     }
1818 
1819     /**
1820      * {@inheritDoc}
1821      */
1822     @Override
1823     public DomAttr setNamedItem(final Node node) {
1824         return put(node.getLocalName(), (DomAttr) node);
1825     }
1826 
1827     /**
1828      * {@inheritDoc}
1829      */
1830     @Override
1831     public Node setNamedItemNS(final Node node) throws DOMException {
1832         return put(node.getNodeName(), (DomAttr) node);
1833     }
1834 
1835     /**
1836      * {@inheritDoc}
1837      */
1838     @Override
1839     public DomAttr put(final String key, final DomAttr value) {
1840         final String name = fixName(key);
1841         return map_.put(name, value);
1842     }
1843 
1844     /**
1845      * {@inheritDoc}
1846      */
1847     @Override
1848     public DomAttr remove(final Object key) {
1849         if (key instanceof String) {
1850             final String name = fixName((String) key);
1851             return map_.remove(name);
1852         }
1853         return null;
1854     }
1855 
1856     /**
1857      * {@inheritDoc}
1858      */
1859     @Override
1860     public void clear() {
1861         map_.clear();
1862     }
1863 
1864     /**
1865      * {@inheritDoc}
1866      */
1867     @Override
1868     public void putAll(final Map<? extends String, ? extends DomAttr> t) {
1869         // add one after the other to save the positions
1870         for (final Map.Entry<? extends String, ? extends DomAttr> entry : t.entrySet()) {
1871             put(entry.getKey(), entry.getValue());
1872         }
1873     }
1874 
1875     /**
1876      * {@inheritDoc}
1877      */
1878     @Override
1879     public boolean containsKey(final Object key) {
1880         if (key instanceof String) {
1881             final String name = fixName((String) key);
1882             return map_.containsKey(name);
1883         }
1884         return false;
1885     }
1886 
1887     /**
1888      * {@inheritDoc}
1889      */
1890     @Override
1891     public DomAttr get(final Object key) {
1892         if (key instanceof String) {
1893             final String name = fixName((String) key);
1894             return map_.get(name);
1895         }
1896         return null;
1897     }
1898 
1899     /**
1900      * Fast access.
1901      * @param key the key
1902      */
1903     protected DomAttr getDirect(final String key) {
1904         return map_.get(key);
1905     }
1906 
1907     /**
1908      * {@inheritDoc}
1909      */
1910     @Override
1911     public boolean containsValue(final Object value) {
1912         return map_.containsValue(value);
1913     }
1914 
1915     /**
1916      * {@inheritDoc}
1917      */
1918     @Override
1919     public Set<Map.Entry<String, DomAttr>> entrySet() {
1920         return map_.entrySet();
1921     }
1922 
1923     /**
1924      * {@inheritDoc}
1925      */
1926     @Override
1927     public boolean isEmpty() {
1928         return map_.isEmpty();
1929     }
1930 
1931     /**
1932      * {@inheritDoc}
1933      */
1934     @Override
1935     public Set<String> keySet() {
1936         return map_.keySet();
1937     }
1938 
1939     /**
1940      * {@inheritDoc}
1941      */
1942     @Override
1943     public int size() {
1944         return map_.size();
1945     }
1946 
1947     /**
1948      * {@inheritDoc}
1949      */
1950     @Override
1951     public Collection<DomAttr> values() {
1952         return map_.values();
1953     }
1954 }