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