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 "<" and ">" (or "/>") 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 "/>")
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() & !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() & !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() & !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 }