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