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         for (final Map.Entry<String, StyleElement> entry : map.entrySet()) {
278             if (entry.getKey().equalsIgnoreCase(name)) {
279                 return entry.getValue();
280             }
281         }
282         return null;
283     }
284 
285     /**
286      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
287      *
288      * Returns a sorted map containing style elements, keyed on style element name. We use a
289      * {@link LinkedHashMap} map so that results are deterministic and are thus testable.
290      *
291      * @return a sorted map containing style elements, keyed on style element name
292      */
293     public LinkedHashMap<String, StyleElement> getStyleMap() {
294         final String styleAttribute = getAttributeDirect("style");
295         if (styleString_ == styleAttribute) {
296             return styleMap_;
297         }
298 
299         final LinkedHashMap<String, StyleElement> styleMap = new LinkedHashMap<>();
300         if (ATTRIBUTE_NOT_DEFINED == styleAttribute || ATTRIBUTE_VALUE_EMPTY == styleAttribute) {
301             styleMap_ = styleMap;
302             styleString_ = styleAttribute;
303             return styleMap_;
304         }
305 
306         final CSSStyleDeclarationImpl cssStyle = new CSSStyleDeclarationImpl(null);
307         try {
308             // use the configured cssErrorHandler here to do the same error handling during
309             // parsing of inline styles like for external css
310             cssStyle.setCssText(styleAttribute, getPage().getWebClient().getCssErrorHandler());
311         }
312         catch (final Exception e) {
313             if (LOG.isErrorEnabled()) {
314                 LOG.error("Error while parsing style value '" + styleAttribute + "'", e);
315             }
316         }
317 
318         for (final Property prop : cssStyle.getProperties()) {
319             final String key = prop.getName().toLowerCase(Locale.ROOT);
320             final StyleElement element = new StyleElement(key,
321                     prop.getValue().getCssText(),
322                     prop.isImportant() ? StyleElement.PRIORITY_IMPORTANT : "",
323                     SelectorSpecificity.FROM_STYLE_ATTRIBUTE);
324             styleMap.put(key, element);
325         }
326 
327         styleMap_ = styleMap;
328         styleString_ = styleAttribute;
329         // styleString_ = cssStyle.getCssText();
330         return styleMap_;
331     }
332 
333     /**
334      * Prints the content between "&lt;" and "&gt;" (or "/&gt;") in the output of the tag name
335      * and its attributes in XML format.
336      * @param printWriter the writer to print in
337      */
338     protected void printOpeningTagContentAsXml(final PrintWriter printWriter) {
339         printWriter.print(getTagName());
340         for (final Map.Entry<String, DomAttr> entry : attributes_.entrySet()) {
341             printWriter.print(" ");
342             printWriter.print(entry.getKey());
343             printWriter.print("=\"");
344             printWriter.print(StringUtils.escapeXmlAttributeValue(entry.getValue().getNodeValue()));
345             printWriter.print("\"");
346         }
347     }
348 
349     /**
350      * {@inheritDoc}
351      */
352     @Override
353     protected boolean printXml(final String indent, final boolean indentBefore, final PrintWriter printWriter) {
354         final boolean hasChildren = getFirstChild() != null;
355 
356         if (indentBefore) {
357             printWriter.print("\r\n");
358             printWriter.print(indent);
359         }
360 
361         printWriter.print('<');
362         printOpeningTagContentAsXml(printWriter);
363 
364         if (hasChildren) {
365             printWriter.print(">");
366             final boolean indBefore = printChildrenAsXml(indent, false, printWriter);
367             if (indBefore) {
368                 printWriter.print("\r\n");
369                 printWriter.print(indent);
370             }
371             printWriter.print("</");
372             printWriter.print(getTagName());
373             printWriter.print(">");
374         }
375         else if (isEmptyXmlTagExpanded()) {
376             printWriter.print("></");
377             printWriter.print(getTagName());
378             printWriter.print(">");
379         }
380         else {
381             printWriter.print("/>");
382         }
383 
384         return false;
385     }
386 
387     /**
388      * Indicates if a node without children should be written in expanded form as XML
389      * (i.e. with closing tag rather than with "/&gt;")
390      * @return {@code false} by default
391      */
392     protected boolean isEmptyXmlTagExpanded() {
393         return false;
394     }
395 
396     /**
397      * Returns the qualified name (prefix:local) for the specified namespace and local name,
398      * or {@code null} if the specified namespace URI does not exist.
399      *
400      * @param namespaceURI the URI that identifies an XML namespace
401      * @param localName the name within the namespace
402      * @return the qualified name for the specified namespace and local name
403      */
404     String getQualifiedName(final String namespaceURI, final String localName) {
405         final String qualifiedName;
406         if (namespaceURI == null) {
407             qualifiedName = localName;
408         }
409         else {
410             final String prefix = namespaces_ == null ? null : namespaces_.get(namespaceURI);
411             if (prefix == null) {
412                 qualifiedName = null;
413             }
414             else {
415                 qualifiedName = prefix + ':' + localName;
416             }
417         }
418         return qualifiedName;
419     }
420 
421     /**
422      * Returns the value of the attribute specified by name or an empty string. If the
423      * result is an empty string then it will be either {@link #ATTRIBUTE_NOT_DEFINED}
424      * if the attribute wasn't specified or {@link #ATTRIBUTE_VALUE_EMPTY} if the
425      * attribute was specified, but it was empty.
426      *
427      * @param attributeName the name of the attribute
428      * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY}
429      */
430     @Override
431     public String getAttribute(final String attributeName) {
432         final DomAttr attr = attributes_.get(attributeName);
433         if (attr != null) {
434             return attr.getNodeValue();
435         }
436         return ATTRIBUTE_NOT_DEFINED;
437     }
438 
439     /**
440      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
441      *
442      * @param attributeName the name of the attribute
443      * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY}
444      */
445     public String getAttributeDirect(final String attributeName) {
446         final DomAttr attr = attributes_.getDirect(attributeName);
447         if (attr != null) {
448             return attr.getNodeValue();
449         }
450         return ATTRIBUTE_NOT_DEFINED;
451     }
452 
453     /**
454      * Removes an attribute specified by name from this element.
455      * @param attributeName the attribute attributeName
456      */
457     @Override
458     public void removeAttribute(final String attributeName) {
459         attributes_.remove(attributeName);
460     }
461 
462     /**
463      * Removes an attribute specified by namespace and local name from this element.
464      * @param namespaceURI the URI that identifies an XML namespace
465      * @param localName the name within the namespace
466      */
467     @Override
468     public final void removeAttributeNS(final String namespaceURI, final String localName) {
469         final String qualifiedName = getQualifiedName(namespaceURI, localName);
470         if (qualifiedName != null) {
471             removeAttribute(qualifiedName);
472         }
473     }
474 
475     /**
476      * {@inheritDoc}
477      * Not yet implemented.
478      */
479     @Override
480     public final Attr removeAttributeNode(final Attr attribute) {
481         throw new UnsupportedOperationException("DomElement.removeAttributeNode is not yet implemented.");
482     }
483 
484     /**
485      * Returns whether the attribute specified by namespace and local name has a value.
486      *
487      * @param namespaceURI the URI that identifies an XML namespace
488      * @param localName the name within the namespace
489      * @return true if an attribute with the given name is specified on this element or has a
490      *         default value, false otherwise.
491      */
492     @Override
493     public final boolean hasAttributeNS(final String namespaceURI, final String localName) {
494         final String qualifiedName = getQualifiedName(namespaceURI, localName);
495         if (qualifiedName != null) {
496             return attributes_.get(qualifiedName) != null;
497         }
498         return false;
499     }
500 
501     /**
502      * Returns the map holding the attributes, keyed by name.
503      * @return the attributes map
504      */
505     public final Map<String, DomAttr> getAttributesMap() {
506         return attributes_;
507     }
508 
509     /**
510      * {@inheritDoc}
511      */
512     @Override
513     public NamedNodeMap getAttributes() {
514         return attributes_;
515     }
516 
517     /**
518      * Sets the value of the attribute specified by name.
519      *
520      * @param attributeName the name of the attribute
521      * @param attributeValue the value of the attribute
522      */
523     @Override
524     public void setAttribute(final String attributeName, final String attributeValue) {
525         setAttributeNS(null, attributeName, attributeValue);
526     }
527 
528     /**
529      * Sets the value of the attribute specified by namespace and qualified name.
530      *
531      * @param namespaceURI the URI that identifies an XML namespace
532      * @param qualifiedName the qualified name (prefix:local) of the attribute
533      * @param attributeValue the value of the attribute
534      */
535     @Override
536     public void setAttributeNS(final String namespaceURI, final String qualifiedName,
537             final String attributeValue) {
538         setAttributeNS(namespaceURI, qualifiedName, attributeValue, true, true);
539     }
540 
541     /**
542      * Sets the value of the attribute specified by namespace and qualified name.
543      *
544      * @param namespaceURI the URI that identifies an XML namespace
545      * @param qualifiedName the qualified name (prefix:local) of the attribute
546      * @param attributeValue the value of the attribute
547      * @param notifyAttributeChangeListeners to notify the associated {@link HtmlAttributeChangeListener}s
548      * @param notifyMutationObservers to notify {@code MutationObserver}s or not
549      */
550     protected void setAttributeNS(final String namespaceURI, final String qualifiedName,
551             final String attributeValue, final boolean notifyAttributeChangeListeners,
552             final boolean notifyMutationObservers) {
553         final DomAttr newAttr = new DomAttr(getPage(), namespaceURI, qualifiedName, attributeValue, true);
554         newAttr.setParentNode(this);
555         attributes_.put(qualifiedName, newAttr);
556 
557         if (namespaceURI != null) {
558             if (namespaces_ == null) {
559                 namespaces_ = new FastHashMap<>(1, 0.5f);
560             }
561             namespaces_.put(namespaceURI, newAttr.getPrefix());
562         }
563     }
564 
565     /**
566      * Indicates if the attribute names are case sensitive.
567      * @return {@code true}
568      */
569     protected boolean isAttributeCaseSensitive() {
570         return true;
571     }
572 
573     /**
574      * Returns the value of the attribute specified by namespace and local name or an empty
575      * string. If the result is an empty string then it will be either {@link #ATTRIBUTE_NOT_DEFINED}
576      * if the attribute wasn't specified or {@link #ATTRIBUTE_VALUE_EMPTY} if the
577      * attribute was specified, but it was empty.
578      *
579      * @param namespaceURI the URI that identifies an XML namespace
580      * @param localName the name within the namespace
581      * @return the value of the attribute or {@link #ATTRIBUTE_NOT_DEFINED} or {@link #ATTRIBUTE_VALUE_EMPTY}
582      */
583     @Override
584     public final String getAttributeNS(final String namespaceURI, final String localName) {
585         final String qualifiedName = getQualifiedName(namespaceURI, localName);
586         if (qualifiedName != null) {
587             return getAttribute(qualifiedName);
588         }
589         return ATTRIBUTE_NOT_DEFINED;
590     }
591 
592     /**
593      * {@inheritDoc}
594      */
595     @Override
596     public DomAttr getAttributeNode(final String name) {
597         return attributes_.get(name);
598     }
599 
600     /**
601      * {@inheritDoc}
602      */
603     @Override
604     public DomAttr getAttributeNodeNS(final String namespaceURI, final String localName) {
605         final String qualifiedName = getQualifiedName(namespaceURI, localName);
606         if (qualifiedName != null) {
607             return attributes_.get(qualifiedName);
608         }
609         return null;
610     }
611 
612     /**
613      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
614      *
615      * @param styleMap the styles
616      */
617     public void writeStyleToElement(final Map<String, StyleElement> styleMap) {
618         if (styleMap.isEmpty()) {
619             setAttribute("style", "");
620             return;
621         }
622 
623         final StringBuilder builder = new StringBuilder();
624         final List<StyleElement> styleElements = new ArrayList<>(styleMap.values());
625         styleElements.sort(STYLE_ELEMENT_COMPARATOR);
626         for (final StyleElement e : styleElements) {
627             if (builder.length() != 0) {
628                 builder.append(' ');
629             }
630             builder.append(e.getName())
631                 .append(": ")
632                 .append(e.getValue());
633 
634             final String prio = e.getPriority();
635             if (StringUtils.isNotBlank(prio)) {
636                 builder.append(" !").append(prio);
637             }
638             builder.append(';');
639         }
640         setAttribute("style", builder.toString());
641     }
642 
643     /**
644      * {@inheritDoc}
645      */
646     @Override
647     public DomNodeList<HtmlElement> getElementsByTagName(final String tagName) {
648         return getElementsByTagNameImpl(tagName);
649     }
650 
651     /**
652      * This should be {@link #getElementsByTagName(String)}, but is separate because of the type erasure in Java.
653      * @param tagName The name of the tag to match on
654      * @return A list of matching elements.
655      */
656     <E extends HtmlElement> DomNodeList<E> getElementsByTagNameImpl(final String tagName) {
657         return new AbstractDomNodeList<>(this) {
658             @Override
659             @SuppressWarnings("unchecked")
660             protected List<E> provideElements() {
661                 final List<E> res = new ArrayList<>();
662                 for (final HtmlElement elem : getDomNode().getHtmlElementDescendants()) {
663                     if (elem.getLocalName().equalsIgnoreCase(tagName)) {
664                         res.add((E) elem);
665                     }
666                 }
667                 return res;
668             }
669         };
670     }
671 
672     /**
673      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
674      *
675      * @param <E> the specific HtmlElement type
676      * @param tagName The name of the tag to match on
677      * @return A list of matching elements; this is not a live list
678      */
679     public <E extends HtmlElement> List<E> getStaticElementsByTagName(final String tagName) {
680         final List<E> res = new ArrayList<>();
681         for (final Iterator<HtmlElement> iterator = this.new DescendantHtmlElementsIterator(); iterator.hasNext();) {
682             final HtmlElement elem = iterator.next();
683             if (elem.getLocalName().equalsIgnoreCase(tagName)) {
684                 final String prefix = elem.getPrefix();
685                 if (prefix == null || prefix.isEmpty()) {
686                     res.add((E) elem);
687                 }
688             }
689         }
690         return res;
691     }
692 
693     /**
694      * {@inheritDoc}
695      * Not yet implemented.
696      */
697     @Override
698     public DomNodeList<HtmlElement> getElementsByTagNameNS(final String namespace, final String localName) {
699         throw new UnsupportedOperationException("DomElement.getElementsByTagNameNS is not yet implemented.");
700     }
701 
702     /**
703      * {@inheritDoc}
704      * Not yet implemented.
705      */
706     @Override
707     public TypeInfo getSchemaTypeInfo() {
708         throw new UnsupportedOperationException("DomElement.getSchemaTypeInfo is not yet implemented.");
709     }
710 
711     /**
712      * {@inheritDoc}
713      * Not yet implemented.
714      */
715     @Override
716     public void setIdAttribute(final String name, final boolean isId) {
717         throw new UnsupportedOperationException("DomElement.setIdAttribute is not yet implemented.");
718     }
719 
720     /**
721      * {@inheritDoc}
722      * Not yet implemented.
723      */
724     @Override
725     public void setIdAttributeNS(final String namespaceURI, final String localName, final boolean isId) {
726         throw new UnsupportedOperationException("DomElement.setIdAttributeNS is not yet implemented.");
727     }
728 
729     /**
730      * {@inheritDoc}
731      */
732     @Override
733     public Attr setAttributeNode(final Attr attribute) {
734         return attributes_.setNamedItem(attribute);
735     }
736 
737     /**
738      * {@inheritDoc}
739      * Not yet implemented.
740      */
741     @Override
742     public Attr setAttributeNodeNS(final Attr attribute) {
743         throw new UnsupportedOperationException("DomElement.setAttributeNodeNS is not yet implemented.");
744     }
745 
746     /**
747      * {@inheritDoc}
748      * Not yet implemented.
749      */
750     @Override
751     public final void setIdAttributeNode(final Attr idAttr, final boolean isId) {
752         throw new UnsupportedOperationException("DomElement.setIdAttributeNode is not yet implemented.");
753     }
754 
755     /**
756      * {@inheritDoc}
757      */
758     @Override
759     public DomNode cloneNode(final boolean deep) {
760         final DomElement clone = (DomElement) super.cloneNode(deep);
761         clone.attributes_ = new NamedAttrNodeMapImpl(clone, isAttributeCaseSensitive());
762         clone.attributes_.putAll(attributes_);
763         return clone;
764     }
765 
766     /**
767      * Returns the identifier of this element.
768      *
769      * @return the identifier of this element
770      */
771     public final String getId() {
772         return getAttributeDirect(ID_ATTRIBUTE);
773     }
774 
775     /**
776      * Sets the identifier this element.
777      *
778      * @param newId the new identifier of this element
779      */
780     public final void setId(final String newId) {
781         setAttribute(ID_ATTRIBUTE, newId);
782     }
783 
784     /**
785      * Returns the first child element node of this element. null if this element has no child elements.
786      * @return the first child element node of this element. null if this element has no child elements
787      */
788     public DomElement getFirstElementChild() {
789         final Iterator<DomElement> i = getChildElements().iterator();
790         if (i.hasNext()) {
791             return i.next();
792         }
793         return null;
794     }
795 
796     /**
797      * Returns the last child element node of this element. null if this element has no child elements.
798      * @return the last child element node of this element. null if this element has no child elements
799      */
800     public DomElement getLastElementChild() {
801         DomElement lastChild = null;
802         for (final DomElement domElement : getChildElements()) {
803             lastChild = domElement;
804         }
805         return lastChild;
806     }
807 
808     /**
809      * Returns the current number of element nodes that are children of this element.
810      * @return the current number of element nodes that are children of this element.
811      */
812     public int getChildElementCount() {
813         int counter = 0;
814 
815         for (final DomElement domElement : getChildElements()) {
816             counter++;
817         }
818         return counter;
819     }
820 
821     /**
822      * Returns an {@link Iterable} over the {@link DomElement} children of this object, i.e. excluding the non-element nodes.
823      *
824      * @return an {@link Iterable} over the {@link DomElement} children of this object, i.e. excluding the non-element nodes
825      */
826     public final Iterable<DomElement> getChildElements() {
827         return new ChildElementsIterable(this);
828     }
829 
830     /**
831      * An Iterable over the DomElement children.
832      */
833     private static class ChildElementsIterable implements Iterable<DomElement> {
834         private final Iterator<DomElement> iterator_;
835 
836         /**
837          * Constructor.
838          * @param domNode the parent
839          */
840         protected ChildElementsIterable(final DomNode domNode) {
841             iterator_ = new ChildElementsIterator(domNode);
842         }
843 
844         @Override
845         public Iterator<DomElement> iterator() {
846             return iterator_;
847         }
848     }
849 
850     /**
851      * An iterator over the DomElement children.
852      */
853     protected static class ChildElementsIterator implements Iterator<DomElement> {
854 
855         private DomElement currentElement_;
856         private DomElement nextElement_;
857 
858         /**
859          * Constructor.
860          * @param domNode the parent
861          */
862         protected ChildElementsIterator(final DomNode domNode) {
863             final DomNode child = domNode.getFirstChild();
864             if (child != null) {
865                 if (child instanceof DomElement element) {
866                     nextElement_ = element;
867                 }
868                 else {
869                     setNextElement(child);
870                 }
871             }
872         }
873 
874         /**
875          * {@inheritDoc}
876          */
877         @Override
878         public boolean hasNext() {
879             return nextElement_ != null;
880         }
881 
882         /**
883          * {@inheritDoc}
884          */
885         @Override
886         public DomElement next() {
887             if (nextElement_ != null) {
888                 currentElement_ = nextElement_;
889                 setNextElement(nextElement_);
890                 return currentElement_;
891             }
892             throw new NoSuchElementException();
893         }
894 
895         /** Removes the current one. */
896         @Override
897         public void remove() {
898             if (currentElement_ == null) {
899                 throw new IllegalStateException();
900             }
901             currentElement_.remove();
902             currentElement_ = null;
903         }
904 
905         private void setNextElement(final DomNode node) {
906             DomNode next = node.getNextSibling();
907             while (next != null && !(next instanceof DomElement)) {
908                 next = next.getNextSibling();
909             }
910             nextElement_ = (DomElement) next;
911         }
912     }
913 
914     /**
915      * Returns a string representation of this element.
916      * @return a string representation of this element
917      */
918     @Override
919     public String toString() {
920         final StringWriter writer = new StringWriter();
921         final PrintWriter printWriter = new PrintWriter(writer);
922 
923         printWriter.print(getClass().getSimpleName());
924         printWriter.print("[<");
925         printOpeningTagContentAsXml(printWriter);
926         printWriter.print(">]");
927         printWriter.flush();
928         return writer.toString();
929     }
930 
931     /**
932      * Simulates clicking on this element, returning the page in the window that has the focus
933      * after the element has been clicked. Note that the returned page may or may not be the same
934      * as the original page, depending on the type of element being clicked, the presence of JavaScript
935      * action listeners, etc.<br>
936      * This only clicks the element if it is visible and enabled (isDisplayed() &amp; !isDisabled()).
937      * In case the element is not visible and/or disabled, only a log output is generated.
938      * <br>
939      * If you circumvent the visible/disabled check use click(shiftKey, ctrlKey, altKey, true, true, false)
940      *
941      * @param <P> the page type
942      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
943      * @exception IOException if an IO error occurs
944      */
945     public <P extends Page> P click() throws IOException {
946         return click(false, false, false);
947     }
948 
949     /**
950      * Simulates clicking on this element, returning the page in the window that has the focus
951      * after the element has been clicked. Note that the returned page may or may not be the same
952      * as the original page, depending on the type of element being clicked, the presence of JavaScript
953      * action listeners, etc.<br>
954      * This only clicks the element if it is visible and enabled (isDisplayed() &amp; !isDisabled()).
955      * In case the element is not visible and/or disabled, only a log output is generated.
956      * <br>
957      * If you circumvent the visible/disabled check use click(shiftKey, ctrlKey, altKey, true, true, false)
958      *
959      * @param shiftKey {@code true} if SHIFT is pressed during the click
960      * @param ctrlKey {@code true} if CTRL is pressed during the click
961      * @param altKey {@code true} if ALT is pressed during the click
962      * @param <P> the page type
963      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
964      * @exception IOException if an IO error occurs
965      */
966     public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey)
967         throws IOException {
968 
969         return click(shiftKey, ctrlKey, altKey, true);
970     }
971 
972     /**
973      * Simulates clicking on this element, returning the page in the window that has the focus
974      * after the element has been clicked. Note that the returned page may or may not be the same
975      * as the original page, depending on the type of element being clicked, the presence of JavaScript
976      * action listeners, etc.<br>
977      * This only clicks the element if it is visible and enabled (isDisplayed() &amp; !isDisabled()).
978      * In case the element is not visible and/or disabled, only a log output is generated.
979      * <br>
980      * If you circumvent the visible/disabled check use click(shiftKey, ctrlKey, altKey, true, true, false)
981      *
982      * @param shiftKey {@code true} if SHIFT is pressed during the click
983      * @param ctrlKey {@code true} if CTRL is pressed during the click
984      * @param altKey {@code true} if ALT is pressed during the click
985      * @param triggerMouseEvents if true trigger the mouse events also
986      * @param <P> the page type
987      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
988      * @exception IOException if an IO error occurs
989      */
990     public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
991             final boolean triggerMouseEvents) throws IOException {
992         return click(shiftKey, ctrlKey, altKey, triggerMouseEvents, true, false, false);
993     }
994 
995     /**
996      * Returns true if this is an {@link DisabledElement} and disabled.
997      *
998      * @return true if this is an {@link DisabledElement} and disabled
999      */
1000     protected boolean isDisabledElementAndDisabled() {
1001         return this instanceof DisabledElement de && de.isDisabled();
1002     }
1003 
1004     /**
1005      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1006      *
1007      * Simulates clicking on this element, returning the page in the window that has the focus
1008      * after the element has been clicked. Note that the returned page may or may not be the same
1009      * as the original page, depending on the type of element being clicked, the presence of JavaScript
1010      * action listeners, etc.
1011      *
1012      * @param shiftKey {@code true} if SHIFT is pressed during the click
1013      * @param ctrlKey {@code true} if CTRL is pressed during the click
1014      * @param altKey {@code true} if ALT is pressed during the click
1015      * @param triggerMouseEvents if true trigger the mouse events also
1016      * @param handleFocus if true set the focus (and trigger the event)
1017      * @param ignoreVisibility whether to ignore visibility or not
1018      * @param disableProcessLabelAfterBubbling ignore label processing
1019      * @param <P> the page type
1020      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
1021      * @exception IOException if an IO error occurs
1022      */
1023     @SuppressWarnings("unchecked")
1024     public <P extends Page> P click(final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
1025             final boolean triggerMouseEvents, final boolean handleFocus, final boolean ignoreVisibility,
1026             final boolean disableProcessLabelAfterBubbling) throws IOException {
1027 
1028         // make enclosing window the current one
1029         final SgmlPage page = getPage();
1030         final WebClient webClient = page.getWebClient();
1031         webClient.setCurrentWindow(page.getEnclosingWindow());
1032 
1033         if (!ignoreVisibility) {
1034             if (!(page instanceof HtmlPage)) {
1035                 return (P) page;
1036             }
1037 
1038             if (!isDisplayed()) {
1039                 if (LOG.isWarnEnabled()) {
1040                     LOG.warn("Calling click() ignored because the target element '" + this
1041                                     + "' is not displayed.");
1042                 }
1043                 return (P) page;
1044             }
1045 
1046             if (isDisabledElementAndDisabled()) {
1047                 if (LOG.isWarnEnabled()) {
1048                     LOG.warn("Calling click() ignored because the target element '" + this + "' is disabled.");
1049                 }
1050                 return (P) page;
1051             }
1052         }
1053 
1054         synchronized (page) {
1055             if (triggerMouseEvents) {
1056                 mouseDown(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT);
1057             }
1058 
1059             final AbstractJavaScriptEngine<?> jsEngine = webClient.getJavaScriptEngine();
1060             if (webClient.isJavaScriptEnabled()) {
1061                 jsEngine.holdPosponedActions();
1062             }
1063             try {
1064                 if (handleFocus) {
1065                     // give focus to current element (if possible) or only remove it from previous one
1066                     DomElement elementToFocus = null;
1067                     if (this instanceof SubmittableElement
1068                         || this instanceof HtmlAnchor anchor
1069                             && ATTRIBUTE_NOT_DEFINED != anchor.getHrefAttribute()
1070                         || this instanceof HtmlArea area
1071                             && (ATTRIBUTE_NOT_DEFINED != area.getHrefAttribute()
1072                                 || webClient.getBrowserVersion().hasFeature(JS_AREA_WITHOUT_HREF_FOCUSABLE))
1073                         || this instanceof HtmlElement && ((HtmlElement) this).getTabIndex() != null) {
1074                         elementToFocus = this;
1075                     }
1076                     else if (this instanceof HtmlOption option) {
1077                         elementToFocus = option.getEnclosingSelect();
1078                     }
1079 
1080                     if (elementToFocus == null) {
1081                         ((HtmlPage) page).setFocusedElement(null);
1082                     }
1083                     else {
1084                         elementToFocus.focus();
1085                     }
1086                 }
1087 
1088                 if (triggerMouseEvents) {
1089                     mouseUp(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_LEFT);
1090                 }
1091 
1092                 MouseEvent event = null;
1093                 if (webClient.isJavaScriptEnabled()) {
1094                     event = new PointerEvent(getEventTargetElement(), MouseEvent.TYPE_CLICK, shiftKey,
1095                             ctrlKey, altKey, MouseEvent.BUTTON_LEFT, 1);
1096 
1097                     if (disableProcessLabelAfterBubbling) {
1098                         event.disableProcessLabelAfterBubbling();
1099                     }
1100                 }
1101                 click(event, shiftKey, ctrlKey, altKey, ignoreVisibility);
1102             }
1103             finally {
1104                 if (webClient.isJavaScriptEnabled()) {
1105                     jsEngine.processPostponedActions();
1106                 }
1107             }
1108 
1109             return (P) webClient.getCurrentWindow().getEnclosedPage();
1110         }
1111     }
1112 
1113     /**
1114      * Returns the event target element. This could be overridden by subclasses to have other targets.
1115      * The default implementation returns 'this'.
1116      * @return the event target element.
1117      */
1118     protected DomNode getEventTargetElement() {
1119         return this;
1120     }
1121 
1122     /**
1123      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1124      *
1125      * Simulates clicking on this element, returning the page in the window that has the focus
1126      * after the element has been clicked. Note that the returned page may or may not be the same
1127      * as the original page, depending on the type of element being clicked, the presence of JavaScript
1128      * action listeners, etc.
1129      *
1130      * @param event the click event used
1131      * @param shiftKey {@code true} if SHIFT is pressed during the click
1132      * @param ctrlKey {@code true} if CTRL is pressed during the click
1133      * @param altKey {@code true} if ALT is pressed during the click
1134      * @param ignoreVisibility whether to ignore visibility or not
1135      * @param <P> the page type
1136      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
1137      * @exception IOException if an IO error occurs
1138      */
1139     @SuppressWarnings("unchecked")
1140     public <P extends Page> P click(final Event event,
1141                         final boolean shiftKey, final boolean ctrlKey, final boolean altKey,
1142                         final boolean ignoreVisibility) throws IOException {
1143         final SgmlPage page = getPage();
1144 
1145         if ((!ignoreVisibility && !isDisplayed()) || isDisabledElementAndDisabled()) {
1146             return (P) page;
1147         }
1148 
1149         final WebClient webClient = page.getWebClient();
1150         if (!webClient.isJavaScriptEnabled()) {
1151             doClickStateUpdate(shiftKey, ctrlKey);
1152 
1153             webClient.loadDownloadedResponses();
1154             return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
1155         }
1156 
1157         // may be different from page when working with "orphaned pages"
1158         // (ex: clicking a link in a page that is not active anymore)
1159         final Page contentPage = page.getEnclosingWindow().getEnclosedPage();
1160 
1161         boolean stateUpdated = false;
1162         boolean changed = false;
1163         if (isStateUpdateFirst()) {
1164             changed = doClickStateUpdate(shiftKey, ctrlKey);
1165             stateUpdated = true;
1166         }
1167 
1168         final ScriptResult scriptResult = doClickFireClickEvent(event);
1169         final boolean eventIsAborted = event.isAborted(scriptResult);
1170 
1171         final boolean pageAlreadyChanged = contentPage != page.getEnclosingWindow().getEnclosedPage();
1172         if (!pageAlreadyChanged && !stateUpdated && !eventIsAborted) {
1173             changed = doClickStateUpdate(shiftKey, ctrlKey);
1174         }
1175 
1176         if (changed) {
1177             doClickFireChangeEvent();
1178         }
1179 
1180         webClient.loadDownloadedResponses();
1181         return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
1182     }
1183 
1184     /**
1185      * This method implements the control state update part of the click action.
1186      *
1187      * <p>The default implementation only calls doClickStateUpdate on parent's DomElement (if any).
1188      * Subclasses requiring different behavior (like {@link HtmlSubmitInput}) will override this method.</p>
1189      * @param shiftKey {@code true} if SHIFT is pressed
1190      * @param ctrlKey {@code true} if CTRL is pressed
1191      *
1192      * @return true if doClickFireEvent method has to be called later on (to signal,
1193      *         that the value was changed)
1194      * @throws IOException if an IO error occurs
1195      */
1196     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
1197         if (propagateClickStateUpdateToParent()) {
1198             // needed for instance to perform link doClickAction when a nested element is clicked
1199             // it should probably be changed to do this at the event level but currently
1200             // this wouldn't work with JS disabled as events are propagated in the host object tree.
1201             final DomNode parent = getParentNode();
1202             if (parent instanceof DomElement element) {
1203                 return element.doClickStateUpdate(false, false);
1204             }
1205         }
1206 
1207         return false;
1208     }
1209 
1210     /**
1211      * Usually the click is propagated to the parent. Overwrite if you like to disable this.
1212      * @return true or false
1213      * @see #doClickStateUpdate(boolean, boolean)
1214      */
1215     protected boolean propagateClickStateUpdateToParent() {
1216         return true;
1217     }
1218 
1219     /**
1220      * This method implements the control onchange handler call during the click action.
1221      */
1222     protected void doClickFireChangeEvent() {
1223         // nothing to do, in the default case
1224     }
1225 
1226     /**
1227      * This method implements the control onclick handler call during the click action.
1228      * @param event the click event used
1229      * @return the script result
1230      */
1231     protected ScriptResult doClickFireClickEvent(final Event event) {
1232         return fireEvent(event);
1233     }
1234 
1235     /**
1236      * Simulates double-clicking on this element, returning the page in the window that has the focus
1237      * after the element has been clicked. Note that the returned page may or may not be the same
1238      * as the original page, depending on the type of element being clicked, the presence of JavaScript
1239      * action listeners, etc. Note also that {@link #click()} is automatically called first.
1240      *
1241      * @param <P> the page type
1242      * @return the page that occupies this element's window after the element has been double-clicked
1243      * @exception IOException if an IO error occurs
1244      */
1245     public <P extends Page> P dblClick() throws IOException {
1246         return dblClick(false, false, false);
1247     }
1248 
1249     /**
1250      * Simulates double-clicking on this element, returning the page in the window that has the focus
1251      * after the element has been clicked. Note that the returned page may or may not be the same
1252      * as the original page, depending on the type of element being clicked, the presence of JavaScript
1253      * action listeners, etc. Note also that {@link #click(boolean, boolean, boolean)} is automatically
1254      * called first.
1255      *
1256      * @param shiftKey {@code true} if SHIFT is pressed during the double click
1257      * @param ctrlKey {@code true} if CTRL is pressed during the double click
1258      * @param altKey {@code true} if ALT is pressed during the double click
1259      * @param <P> the page type
1260      * @return the page that occupies this element's window after the element has been double-clicked
1261      * @exception IOException if an IO error occurs
1262      */
1263     @SuppressWarnings("unchecked")
1264     public <P extends Page> P dblClick(final boolean shiftKey, final boolean ctrlKey, final boolean altKey)
1265         throws IOException {
1266         if (isDisabledElementAndDisabled()) {
1267             return (P) getPage();
1268         }
1269 
1270         // call click event first
1271         P clickPage = click(shiftKey, ctrlKey, altKey);
1272         if (clickPage != getPage()) {
1273             LOG.debug("dblClick() is ignored, as click() loaded a different page.");
1274             return clickPage;
1275         }
1276 
1277         // call click event a second time
1278         clickPage = click(shiftKey, ctrlKey, altKey);
1279         if (clickPage != getPage()) {
1280             LOG.debug("dblClick() is ignored, as click() loaded a different page.");
1281             return clickPage;
1282         }
1283 
1284         final Event event;
1285         event = new MouseEvent(this, MouseEvent.TYPE_DBL_CLICK, shiftKey, ctrlKey, altKey,
1286                 MouseEvent.BUTTON_LEFT, 2);
1287 
1288         final ScriptResult scriptResult = fireEvent(event);
1289         if (scriptResult == null) {
1290             return clickPage;
1291         }
1292         return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
1293     }
1294 
1295     /**
1296      * Simulates moving the mouse over this element, returning the page which this element's window contains
1297      * after the mouse move. The returned page may or may not be the same as the original page, depending
1298      * on JavaScript event handlers, etc.
1299      *
1300      * @return the page which this element's window contains after the mouse move
1301      */
1302     public Page mouseOver() {
1303         return mouseOver(false, false, false, MouseEvent.BUTTON_LEFT);
1304     }
1305 
1306     /**
1307      * Simulates moving the mouse over this element, returning the page which this element's window contains
1308      * after the mouse move. The returned page may or may not be the same as the original page, depending
1309      * on JavaScript event handlers, etc.
1310      *
1311      * @param shiftKey {@code true} if SHIFT is pressed during the mouse move
1312      * @param ctrlKey {@code true} if CTRL is pressed during the mouse move
1313      * @param altKey {@code true} if ALT is pressed during the mouse move
1314      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1315      *        or {@link MouseEvent#BUTTON_RIGHT}
1316      * @return the page which this element's window contains after the mouse move
1317      */
1318     public Page mouseOver(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1319         return doMouseEvent(MouseEvent.TYPE_MOUSE_OVER, shiftKey, ctrlKey, altKey, button);
1320     }
1321 
1322     /**
1323      * Simulates moving the mouse over this element, returning the page which this element's window contains
1324      * after the mouse move. The returned page may or may not be the same as the original page, depending
1325      * on JavaScript event handlers, etc.
1326      *
1327      * @return the page which this element's window contains after the mouse move
1328      */
1329     public Page mouseMove() {
1330         return mouseMove(false, false, false, MouseEvent.BUTTON_LEFT);
1331     }
1332 
1333     /**
1334      * Simulates moving the mouse over this element, returning the page which this element's window contains
1335      * after the mouse move. The returned page may or may not be the same as the original page, depending
1336      * on JavaScript event handlers, etc.
1337      *
1338      * @param shiftKey {@code true} if SHIFT is pressed during the mouse move
1339      * @param ctrlKey {@code true} if CTRL is pressed during the mouse move
1340      * @param altKey {@code true} if ALT is pressed during the mouse move
1341      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1342      *        or {@link MouseEvent#BUTTON_RIGHT}
1343      * @return the page which this element's window contains after the mouse move
1344      */
1345     public Page mouseMove(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1346         return doMouseEvent(MouseEvent.TYPE_MOUSE_MOVE, shiftKey, ctrlKey, altKey, button);
1347     }
1348 
1349     /**
1350      * Simulates moving the mouse out of this element, returning the page which this element's window contains
1351      * after the mouse move. The returned page may or may not be the same as the original page, depending
1352      * on JavaScript event handlers, etc.
1353      *
1354      * @return the page which this element's window contains after the mouse move
1355      */
1356     public Page mouseOut() {
1357         return mouseOut(false, false, false, MouseEvent.BUTTON_LEFT);
1358     }
1359 
1360     /**
1361      * Simulates moving the mouse out of this element, returning the page which this element's window contains
1362      * after the mouse move. The returned page may or may not be the same as the original page, depending
1363      * on JavaScript event handlers, etc.
1364      *
1365      * @param shiftKey {@code true} if SHIFT is pressed during the mouse move
1366      * @param ctrlKey {@code true} if CTRL is pressed during the mouse move
1367      * @param altKey {@code true} if ALT is pressed during the mouse move
1368      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1369      *        or {@link MouseEvent#BUTTON_RIGHT}
1370      * @return the page which this element's window contains after the mouse move
1371      */
1372     public Page mouseOut(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1373         return doMouseEvent(MouseEvent.TYPE_MOUSE_OUT, shiftKey, ctrlKey, altKey, button);
1374     }
1375 
1376     /**
1377      * Simulates clicking the mouse on this element, returning the page which this element's window contains
1378      * after the mouse click. The returned page may or may not be the same as the original page, depending
1379      * on JavaScript event handlers, etc.
1380      *
1381      * @return the page which this element's window contains after the mouse click
1382      */
1383     public Page mouseDown() {
1384         return mouseDown(false, false, false, MouseEvent.BUTTON_LEFT);
1385     }
1386 
1387     /**
1388      * Simulates clicking the mouse on this element, returning the page which this element's window contains
1389      * after the mouse click. The returned page may or may not be the same as the original page, depending
1390      * on JavaScript event handlers, etc.
1391      *
1392      * @param shiftKey {@code true} if SHIFT is pressed during the mouse click
1393      * @param ctrlKey {@code true} if CTRL is pressed during the mouse click
1394      * @param altKey {@code true} if ALT is pressed during the mouse click
1395      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1396      *        or {@link MouseEvent#BUTTON_RIGHT}
1397      * @return the page which this element's window contains after the mouse click
1398      */
1399     public Page mouseDown(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1400         return doMouseEvent(MouseEvent.TYPE_MOUSE_DOWN, shiftKey, ctrlKey, altKey, button);
1401     }
1402 
1403     /**
1404      * Simulates releasing the mouse click on this element, returning the page which this element's window contains
1405      * after the mouse click release. The returned page may or may not be the same as the original page, depending
1406      * on JavaScript event handlers, etc.
1407      *
1408      * @return the page which this element's window contains after the mouse click release
1409      */
1410     public Page mouseUp() {
1411         return mouseUp(false, false, false, MouseEvent.BUTTON_LEFT);
1412     }
1413 
1414     /**
1415      * Simulates releasing the mouse click on this element, returning the page which this element's window contains
1416      * after the mouse click release. The returned page may or may not be the same as the original page, depending
1417      * on JavaScript event handlers, etc.
1418      *
1419      * @param shiftKey {@code true} if SHIFT is pressed during the mouse click release
1420      * @param ctrlKey {@code true} if CTRL is pressed during the mouse click release
1421      * @param altKey {@code true} if ALT is pressed during the mouse click release
1422      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1423      *        or {@link MouseEvent#BUTTON_RIGHT}
1424      * @return the page which this element's window contains after the mouse click release
1425      */
1426     public Page mouseUp(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
1427         return doMouseEvent(MouseEvent.TYPE_MOUSE_UP, shiftKey, ctrlKey, altKey, button);
1428     }
1429 
1430     /**
1431      * Simulates right clicking the mouse on this element, returning the page which this element's window
1432      * contains after the mouse click. The returned page may or may not be the same as the original page,
1433      * depending on JavaScript event handlers, etc.
1434      *
1435      * @return the page which this element's window contains after the mouse click
1436      */
1437     public Page rightClick() {
1438         return rightClick(false, false, false);
1439     }
1440 
1441     /**
1442      * Simulates right clicking the mouse on this element, returning the page which this element's window
1443      * contains after the mouse click. The returned page may or may not be the same as the original page,
1444      * depending on JavaScript event handlers, etc.
1445      *
1446      * @param shiftKey {@code true} if SHIFT is pressed during the mouse click
1447      * @param ctrlKey {@code true} if CTRL is pressed during the mouse click
1448      * @param altKey {@code true} if ALT is pressed during the mouse click
1449      * @return the page which this element's window contains after the mouse click
1450      */
1451     public Page rightClick(final boolean shiftKey, final boolean ctrlKey, final boolean altKey) {
1452         final Page mouseDownPage = mouseDown(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1453         if (mouseDownPage != getPage()) {
1454             LOG.debug("rightClick() is incomplete, as mouseDown() loaded a different page.");
1455             return mouseDownPage;
1456         }
1457 
1458         final Page mouseUpPage = mouseUp(shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1459         if (mouseUpPage != getPage()) {
1460             LOG.debug("rightClick() is incomplete, as mouseUp() loaded a different page.");
1461             return mouseUpPage;
1462         }
1463 
1464         return doMouseEvent(MouseEvent.TYPE_CONTEXT_MENU, shiftKey, ctrlKey, altKey, MouseEvent.BUTTON_RIGHT);
1465     }
1466 
1467     /**
1468      * Simulates the specified mouse event, returning the page which this element's window contains after the event.
1469      * The returned page may or may not be the same as the original page, depending on JavaScript event handlers, etc.
1470      *
1471      * @param eventType the mouse event type to simulate
1472      * @param shiftKey {@code true} if SHIFT is pressed during the mouse event
1473      * @param ctrlKey {@code true} if CTRL is pressed during the mouse event
1474      * @param altKey {@code true} if ALT is pressed during the mouse event
1475      * @param button the button code, must be {@link MouseEvent#BUTTON_LEFT}, {@link MouseEvent#BUTTON_MIDDLE}
1476      *        or {@link MouseEvent#BUTTON_RIGHT}
1477      * @return the page which this element's window contains after the event
1478      */
1479     private Page doMouseEvent(final String eventType, final boolean shiftKey, final boolean ctrlKey,
1480         final boolean altKey, final int button) {
1481         final SgmlPage page = getPage();
1482         final WebClient webClient = getPage().getWebClient();
1483         if (!webClient.isJavaScriptEnabled()) {
1484             return page;
1485         }
1486 
1487         final ScriptResult scriptResult;
1488         final Event event;
1489         if (MouseEvent.TYPE_CONTEXT_MENU.equals(eventType)) {
1490             final BrowserVersion browserVersion = webClient.getBrowserVersion();
1491             if (browserVersion.hasFeature(EVENT_CONTEXT_MENU_HAS_DETAIL_1)) {
1492                 event = new PointerEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 1);
1493             }
1494             else {
1495                 event = new PointerEvent(this, eventType, shiftKey, ctrlKey, altKey, button, 0);
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 anchor && ATTRIBUTE_NOT_DEFINED != anchor.getHrefAttribute()
1583             || this instanceof HtmlArea area
1584                 && (ATTRIBUTE_NOT_DEFINED != area.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 map) {
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_ = map;
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 string) {
1844             final String name = fixName(string);
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 string) {
1875             final String name = fixName(string);
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 string) {
1887             final String name = fixName(string);
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 }