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