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