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