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