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