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.HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT;
18 import static org.htmlunit.BrowserVersionFeatures.KEYBOARD_EVENT_SPECIAL_KEYPRESS;
19 import static org.htmlunit.css.CssStyleSheet.ABSOLUTE;
20 import static org.htmlunit.css.CssStyleSheet.FIXED;
21 import static org.htmlunit.css.CssStyleSheet.STATIC;
22
23 import java.io.IOException;
24 import java.util.ArrayList;
25 import java.util.List;
26 import java.util.Locale;
27 import java.util.Map;
28
29 import org.apache.commons.lang3.StringUtils;
30 import org.htmlunit.BrowserVersion;
31 import org.htmlunit.ElementNotFoundException;
32 import org.htmlunit.Page;
33 import org.htmlunit.ScriptResult;
34 import org.htmlunit.SgmlPage;
35 import org.htmlunit.WebAssert;
36 import org.htmlunit.WebClient;
37 import org.htmlunit.WebWindow;
38 import org.htmlunit.css.ComputedCssStyleDeclaration;
39 import org.htmlunit.html.impl.SelectableTextInput;
40 import org.htmlunit.javascript.HtmlUnitScriptable;
41 import org.htmlunit.javascript.host.dom.Document;
42 import org.htmlunit.javascript.host.dom.MutationObserver;
43 import org.htmlunit.javascript.host.event.Event;
44 import org.htmlunit.javascript.host.event.EventTarget;
45 import org.htmlunit.javascript.host.event.KeyboardEvent;
46 import org.htmlunit.javascript.host.html.HTMLDocument;
47 import org.htmlunit.javascript.host.html.HTMLElement;
48 import org.w3c.dom.Attr;
49 import org.w3c.dom.CDATASection;
50 import org.w3c.dom.Comment;
51 import org.w3c.dom.DOMException;
52 import org.w3c.dom.Element;
53 import org.w3c.dom.EntityReference;
54 import org.w3c.dom.Node;
55 import org.w3c.dom.ProcessingInstruction;
56 import org.w3c.dom.Text;
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78 public abstract class HtmlElement extends DomElement {
79
80
81
82
83 public enum DisplayStyle {
84
85 EMPTY(""),
86
87 NONE("none"),
88
89 BLOCK("block"),
90
91 CONTENTS("contents"),
92
93 INLINE("inline"),
94
95 INLINE_BLOCK("inline-block"),
96
97 LIST_ITEM("list-item"),
98
99 TABLE("table"),
100
101 TABLE_CELL("table-cell"),
102
103 TABLE_COLUMN("table-column"),
104
105 TABLE_COLUMN_GROUP("table-column-group"),
106
107 TABLE_ROW("table-row"),
108
109 TABLE_ROW_GROUP("table-row-group"),
110
111 TABLE_HEADER_GROUP("table-header-group"),
112
113 TABLE_FOOTER_GROUP("table-footer-group"),
114
115 TABLE_CAPTION("table-caption"),
116
117 RUBY("ruby"),
118
119 RUBY_BASE("ruby-base"),
120
121 RUBY_TEXT("ruby-text"),
122
123 RUBY_TEXT_CONTAINER("ruby-text-container");
124
125 private final String value_;
126 DisplayStyle(final String value) {
127 value_ = value;
128 }
129
130
131
132
133
134 public String value() {
135 return value_;
136 }
137 }
138
139
140
141
142
143
144
145 public static final Short TAB_INDEX_OUT_OF_BOUNDS = Short.valueOf(Short.MIN_VALUE);
146
147
148 protected static final String ATTRIBUTE_REQUIRED = "required";
149
150 protected static final String ATTRIBUTE_CHECKED = "checked";
151
152 protected static final String ATTRIBUTE_HIDDEN = "hidden";
153
154
155 private final List<HtmlAttributeChangeListener> attributeListeners_ = new ArrayList<>();
156
157
158 private HtmlForm owningForm_;
159
160 private boolean shiftPressed_;
161 private boolean ctrlPressed_;
162 private boolean altPressed_;
163
164
165
166
167
168
169
170
171
172 protected HtmlElement(final String qualifiedName, final SgmlPage page,
173 final Map<String, DomAttr> attributes) {
174 this(Html.XHTML_NAMESPACE, qualifiedName, page, attributes);
175 }
176
177
178
179
180
181
182
183
184
185
186 protected HtmlElement(final String namespaceURI, final String qualifiedName, final SgmlPage page,
187 final Map<String, DomAttr> attributes) {
188 super(namespaceURI, qualifiedName, page, attributes);
189 }
190
191
192
193
194 @Override
195 protected void setAttributeNS(final String namespaceURI, final String qualifiedName,
196 final String attributeValue, final boolean notifyAttributeChangeListeners,
197 final boolean notifyMutationObservers) {
198
199 final HtmlPage htmlPage = getHtmlPageOrNull();
200
201
202 if (null == htmlPage) {
203 super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
204 notifyMutationObservers);
205 return;
206 }
207
208 final String oldAttributeValue = getAttribute(qualifiedName);
209 final boolean mappedElement = isAttachedToPage()
210 && (DomElement.NAME_ATTRIBUTE.equals(qualifiedName) || DomElement.ID_ATTRIBUTE.equals(qualifiedName));
211 if (mappedElement) {
212
213 htmlPage.removeMappedElement(this, false, false);
214 }
215
216 final HtmlAttributeChangeEvent event;
217 if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
218 event = new HtmlAttributeChangeEvent(this, qualifiedName, attributeValue);
219 }
220 else {
221 event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
222 }
223
224 super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
225 notifyMutationObservers);
226
227 if (notifyAttributeChangeListeners) {
228 notifyAttributeChangeListeners(event, this, oldAttributeValue, notifyMutationObservers);
229 }
230
231 fireAttributeChangeImpl(event, htmlPage, mappedElement, oldAttributeValue);
232 }
233
234
235
236
237
238
239
240
241 protected static void notifyAttributeChangeListeners(final HtmlAttributeChangeEvent event,
242 final HtmlElement element, final String oldAttributeValue, final boolean notifyMutationObservers) {
243 final List<HtmlAttributeChangeListener> listeners = new ArrayList<>(element.attributeListeners_);
244 if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
245 synchronized (listeners) {
246 for (final HtmlAttributeChangeListener listener : listeners) {
247 if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
248 listener.attributeAdded(event);
249 }
250 }
251 }
252 }
253 else {
254 synchronized (listeners) {
255 for (final HtmlAttributeChangeListener listener : listeners) {
256 if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
257 listener.attributeReplaced(event);
258 }
259 }
260 }
261 }
262 final DomNode parentNode = element.getParentNode();
263 if (parentNode instanceof HtmlElement) {
264 notifyAttributeChangeListeners(event, (HtmlElement) parentNode, oldAttributeValue, notifyMutationObservers);
265 }
266 }
267
268 private void fireAttributeChangeImpl(final HtmlAttributeChangeEvent event,
269 final HtmlPage htmlPage, final boolean mappedElement, final String oldAttributeValue) {
270 if (mappedElement) {
271 htmlPage.addMappedElement(this, false);
272 }
273
274 if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
275 fireHtmlAttributeAdded(event);
276 htmlPage.fireHtmlAttributeAdded(event);
277 }
278 else {
279 fireHtmlAttributeReplaced(event);
280 htmlPage.fireHtmlAttributeReplaced(event);
281 }
282 }
283
284
285
286
287
288
289
290
291
292
293 @Override
294 public Attr setAttributeNode(final Attr attribute) {
295 final HtmlPage htmlPage = getHtmlPageOrNull();
296
297
298 if (null == htmlPage) {
299 return super.setAttributeNode(attribute);
300 }
301
302 final String qualifiedName = attribute.getName();
303 final String oldAttributeValue = getAttribute(qualifiedName);
304
305 final boolean mappedElement = isAttachedToPage()
306 && (DomElement.NAME_ATTRIBUTE.equals(qualifiedName)
307 || DomElement.ID_ATTRIBUTE.equals(qualifiedName));
308 if (mappedElement) {
309 htmlPage.removeMappedElement(this, false, false);
310 }
311
312 final HtmlAttributeChangeEvent event;
313 if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
314 event = new HtmlAttributeChangeEvent(this, qualifiedName, attribute.getValue());
315 }
316 else {
317 event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
318 }
319 notifyAttributeChangeListeners(event, this, oldAttributeValue, true);
320
321 final Attr result = super.setAttributeNode(attribute);
322
323 fireAttributeChangeImpl(event, htmlPage, mappedElement, oldAttributeValue);
324
325 return result;
326 }
327
328
329
330
331
332 @Override
333 public void removeAttribute(final String attributeName) {
334 final String value = getAttribute(attributeName);
335 if (ATTRIBUTE_NOT_DEFINED == value) {
336 return;
337 }
338
339 final HtmlPage htmlPage = getHtmlPageOrNull();
340
341
342 if (null == htmlPage) {
343 super.removeAttribute(attributeName);
344 return;
345 }
346
347 final boolean mapped = DomElement.NAME_ATTRIBUTE.equals(attributeName)
348 || DomElement.ID_ATTRIBUTE.equals(attributeName);
349 if (mapped) {
350 htmlPage.removeMappedElement(this, false, false);
351 }
352
353 super.removeAttribute(attributeName);
354
355 if (mapped) {
356 htmlPage.addMappedElement(this, false);
357 }
358
359 final HtmlAttributeChangeEvent event = new HtmlAttributeChangeEvent(this, attributeName, value);
360 fireHtmlAttributeRemoved(event);
361 htmlPage.fireHtmlAttributeRemoved(event);
362 }
363
364
365
366
367
368
369
370
371
372
373
374
375 protected void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) {
376 final DomNode parentNode = getParentNode();
377 if (parentNode instanceof HtmlElement) {
378 ((HtmlElement) parentNode).fireHtmlAttributeAdded(event);
379 }
380 }
381
382
383
384
385
386
387
388
389
390
391
392
393 protected void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) {
394 final DomNode parentNode = getParentNode();
395 if (parentNode instanceof HtmlElement) {
396 ((HtmlElement) parentNode).fireHtmlAttributeReplaced(event);
397 }
398 }
399
400
401
402
403
404
405
406
407
408
409
410
411 protected void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) {
412 synchronized (attributeListeners_) {
413 for (final HtmlAttributeChangeListener listener : attributeListeners_) {
414 listener.attributeRemoved(event);
415 }
416 }
417 final DomNode parentNode = getParentNode();
418 if (parentNode instanceof HtmlElement) {
419 ((HtmlElement) parentNode).fireHtmlAttributeRemoved(event);
420 }
421 }
422
423
424
425
426 @Override
427 public String getNodeName() {
428 final String prefix = getPrefix();
429 if (prefix != null) {
430
431 final StringBuilder name = new StringBuilder(prefix.toLowerCase(Locale.ROOT))
432 .append(':')
433 .append(getLocalName().toLowerCase(Locale.ROOT));
434 return name.toString();
435 }
436 return getLocalName().toLowerCase(Locale.ROOT);
437 }
438
439
440
441
442
443
444
445
446
447 public Short getTabIndex() {
448 final String index = getAttributeDirect("tabindex");
449 if (index == null || index.isEmpty()) {
450 return null;
451 }
452 try {
453 final long l = Long.parseLong(index);
454 if (l >= 0 && l <= Short.MAX_VALUE) {
455 return Short.valueOf((short) l);
456 }
457 return TAB_INDEX_OUT_OF_BOUNDS;
458 }
459 catch (final NumberFormatException e) {
460 return null;
461 }
462 }
463
464
465
466
467
468
469
470 public HtmlElement getEnclosingElement(final String tagName) {
471 final String tagNameLC = tagName.toLowerCase(Locale.ROOT);
472
473 for (DomNode currentNode = getParentNode(); currentNode != null; currentNode = currentNode.getParentNode()) {
474 if (currentNode instanceof HtmlElement && currentNode.getNodeName().equals(tagNameLC)) {
475 return (HtmlElement) currentNode;
476 }
477 }
478 return null;
479 }
480
481
482
483
484
485
486 public HtmlForm getEnclosingForm() {
487 final String formId = getAttribute("form");
488 if (ATTRIBUTE_NOT_DEFINED != formId) {
489 final Element formById = getPage().getElementById(formId);
490 if (formById instanceof HtmlForm) {
491 return (HtmlForm) formById;
492 }
493 return null;
494 }
495
496 if (owningForm_ != null) {
497 return owningForm_;
498 }
499 return (HtmlForm) getEnclosingElement("form");
500 }
501
502
503
504
505
506
507 public HtmlForm getEnclosingFormOrDie() {
508 final HtmlForm form = getEnclosingForm();
509 if (form == null) {
510 throw new IllegalStateException("Element is not contained within a form: " + this);
511 }
512 return form;
513 }
514
515
516
517
518
519
520
521 public void type(final String text) throws IOException {
522 for (final char ch : text.toCharArray()) {
523 type(ch);
524 }
525 }
526
527
528
529
530
531
532
533
534
535
536
537 public Page type(final char c) throws IOException {
538 return type(c, true);
539 }
540
541
542
543
544
545
546
547
548
549
550
551
552 private Page type(final char c, final boolean lastType)
553 throws IOException {
554 if (isDisabledElementAndDisabled()) {
555 return getPage();
556 }
557
558
559 getPage().getWebClient().setCurrentWindow(getPage().getEnclosingWindow());
560
561 final HtmlPage page = (HtmlPage) getPage();
562 if (page.getFocusedElement() != this) {
563 focus();
564 }
565 final boolean isShiftNeeded = KeyboardEvent.isShiftNeeded(c, shiftPressed_);
566
567 final Event shiftDown;
568 final ScriptResult shiftDownResult;
569 if (isShiftNeeded) {
570 shiftDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, KeyboardEvent.DOM_VK_SHIFT,
571 true, ctrlPressed_, altPressed_);
572 shiftDownResult = fireEvent(shiftDown);
573 }
574 else {
575 shiftDown = null;
576 shiftDownResult = null;
577 }
578
579 final Event keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, c,
580 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
581 final ScriptResult keyDownResult = fireEvent(keyDown);
582
583 if (!keyDown.isAborted(keyDownResult)) {
584 final Event keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, c,
585 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
586 final ScriptResult keyPressResult = fireEvent(keyPress);
587
588 if ((shiftDown == null || !shiftDown.isAborted(shiftDownResult))
589 && !keyPress.isAborted(keyPressResult)) {
590 doType(c, lastType);
591 }
592 }
593
594 final WebClient webClient = page.getWebClient();
595 if (this instanceof HtmlTextInput
596 || this instanceof HtmlTextArea
597 || this instanceof HtmlTelInput
598 || this instanceof HtmlNumberInput
599 || this instanceof HtmlSearchInput
600 || this instanceof HtmlPasswordInput) {
601 fireEvent(new KeyboardEvent(this, Event.TYPE_INPUT, c,
602 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_));
603 }
604
605 HtmlElement eventSource = this;
606 if (!isAttachedToPage()) {
607 eventSource = page.getBody();
608 }
609
610 if (eventSource != null) {
611 final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, c,
612 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
613 eventSource.fireEvent(keyUp);
614
615 if (isShiftNeeded) {
616 final Event shiftUp = new KeyboardEvent(this, Event.TYPE_KEY_UP,
617 KeyboardEvent.DOM_VK_SHIFT,
618 false, ctrlPressed_, altPressed_);
619 eventSource.fireEvent(shiftUp);
620 }
621 }
622
623 final HtmlForm form = getEnclosingForm();
624 if (form != null && c == '\n' && isSubmittableByEnter()) {
625 for (final DomElement descendant : form.getDomElementDescendants()) {
626 if (descendant instanceof HtmlSubmitInput) {
627 return descendant.click();
628 }
629 }
630
631 form.submit((SubmittableElement) this);
632 webClient.getJavaScriptEngine().processPostponedActions();
633 }
634 return webClient.getCurrentWindow().getEnclosedPage();
635 }
636
637
638
639
640
641
642
643
644
645
646
647
648
649 public Page type(final int keyCode) {
650 return type(keyCode, true, true, true, true);
651 }
652
653
654
655
656
657
658
659
660
661
662
663
664 public Page type(final Keyboard keyboard) throws IOException {
665 Page page = null;
666
667 final List<Object[]> keys = keyboard.getKeys();
668
669 if (keyboard.isStartAtEnd()) {
670 if (this instanceof SelectableTextInput) {
671 final SelectableTextInput textInput = (SelectableTextInput) this;
672 textInput.setSelectionStart(textInput.getText().length());
673 }
674 else {
675 final DomText domText = getDoTypeNode();
676 if (domText != null) {
677 domText.moveSelectionToEnd();
678 }
679 }
680 }
681
682 final int size = keys.size();
683 for (int i = 0; i < size; i++) {
684 final Object[] entry = keys.get(i);
685 if (entry.length == 1) {
686 type((char) entry[0], i == keys.size() - 1);
687 }
688 else {
689 final int key = (int) entry[0];
690 final boolean pressed = (boolean) entry[1];
691 switch (key) {
692 case KeyboardEvent.DOM_VK_SHIFT:
693 shiftPressed_ = pressed;
694 break;
695
696 case KeyboardEvent.DOM_VK_CONTROL:
697 ctrlPressed_ = pressed;
698 break;
699
700 case KeyboardEvent.DOM_VK_ALT:
701 altPressed_ = pressed;
702 break;
703
704 default:
705 }
706 if (pressed) {
707 boolean keyPress = true;
708 boolean keyUp = true;
709 switch (key) {
710 case KeyboardEvent.DOM_VK_SHIFT:
711 case KeyboardEvent.DOM_VK_CONTROL:
712 case KeyboardEvent.DOM_VK_ALT:
713 keyPress = false;
714 keyUp = false;
715 break;
716
717 default:
718 }
719 page = type(key, true, keyPress, keyUp, i == keys.size() - 1);
720 }
721 else {
722 page = type(key, false, false, true, i == keys.size() - 1);
723 }
724 }
725 }
726
727 return page;
728 }
729
730 private Page type(final int keyCode,
731 final boolean fireKeyDown, final boolean fireKeyPress, final boolean fireKeyUp,
732 final boolean lastType) {
733 if (isDisabledElementAndDisabled()) {
734 return getPage();
735 }
736
737 final HtmlPage page = (HtmlPage) getPage();
738 if (page.getFocusedElement() != this) {
739 focus();
740 }
741
742 final Event keyDown;
743 final ScriptResult keyDownResult;
744 if (fireKeyDown) {
745 keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, keyCode, shiftPressed_, ctrlPressed_, altPressed_);
746 keyDownResult = fireEvent(keyDown);
747 }
748 else {
749 keyDown = null;
750 keyDownResult = null;
751 }
752
753 final BrowserVersion browserVersion = page.getWebClient().getBrowserVersion();
754
755 final Event keyPress;
756 final ScriptResult keyPressResult;
757 if (fireKeyPress && browserVersion.hasFeature(KEYBOARD_EVENT_SPECIAL_KEYPRESS)) {
758 keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, keyCode,
759 shiftPressed_, ctrlPressed_, altPressed_);
760
761 keyPressResult = fireEvent(keyPress);
762 }
763 else {
764 keyPress = null;
765 keyPressResult = null;
766 }
767
768 if (keyDown != null && !keyDown.isAborted(keyDownResult)
769 && (keyPress == null || !keyPress.isAborted(keyPressResult))) {
770 doType(keyCode, lastType);
771 }
772
773 if (this instanceof HtmlTextInput
774 || this instanceof HtmlTextArea
775 || this instanceof HtmlTelInput
776 || this instanceof HtmlNumberInput
777 || this instanceof HtmlSearchInput
778 || this instanceof HtmlPasswordInput) {
779 final Event input = new KeyboardEvent(this, Event.TYPE_INPUT, keyCode,
780 shiftPressed_, ctrlPressed_, altPressed_);
781 fireEvent(input);
782 }
783
784 if (fireKeyUp) {
785 final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, keyCode,
786 shiftPressed_, ctrlPressed_, altPressed_);
787 fireEvent(keyUp);
788 }
789
790
791
792
793
794
795
796
797
798
799
800
801
802 return page.getWebClient().getCurrentWindow().getEnclosedPage();
803 }
804
805
806
807
808
809
810 protected void doType(final char c, final boolean lastType) {
811 final DomText domText = getDoTypeNode();
812 if (domText != null) {
813 domText.doType(c, this, lastType);
814 }
815 }
816
817
818
819
820
821
822
823
824
825 protected void doType(final int keyCode, final boolean lastType) {
826 final DomText domText = getDoTypeNode();
827 if (domText != null) {
828 domText.doType(keyCode, this, lastType);
829 }
830 }
831
832
833
834
835
836 private DomText getDoTypeNode() {
837 final HTMLElement scriptElement = getScriptableObject();
838 if (scriptElement.isIsContentEditable()
839 || "on".equals(((Document) scriptElement.getOwnerDocument()).getDesignMode())) {
840
841 DomNodeList<DomNode> children = getChildNodes();
842 while (!children.isEmpty()) {
843 final DomNode lastChild = children.get(children.size() - 1);
844 if (lastChild instanceof DomText) {
845 return (DomText) lastChild;
846 }
847 children = lastChild.getChildNodes();
848 }
849
850 final DomText domText = new DomText(getPage(), "");
851 appendChild(domText);
852 return domText;
853 }
854 return null;
855 }
856
857
858
859
860
861
862 protected void typeDone(final String newValue, final boolean notifyAttributeChangeListeners) {
863
864 }
865
866
867
868
869
870
871 protected boolean acceptChar(final char c) {
872
873
874 return (c < '\uE000' || c > '\uF8FF')
875 && (c == ' ' || c == '\t' || c == '\u3000' || c == '\u2006' || !Character.isWhitespace(c));
876 }
877
878
879
880
881
882
883 protected boolean isSubmittableByEnter() {
884 return false;
885 }
886
887
888
889
890
891
892
893
894
895
896
897
898 public final <E extends HtmlElement> E getOneHtmlElementByAttribute(final String elementName,
899 final String attributeName,
900 final String attributeValue) throws ElementNotFoundException {
901
902 WebAssert.notNull("elementName", elementName);
903 WebAssert.notNull("attributeName", attributeName);
904 WebAssert.notNull("attributeValue", attributeValue);
905
906 final List<E> list = getElementsByAttribute(elementName, attributeName, attributeValue);
907
908 if (list.isEmpty()) {
909 throw new ElementNotFoundException(elementName, attributeName, attributeValue);
910 }
911
912 return list.get(0);
913 }
914
915
916
917
918
919
920
921
922
923
924 @SuppressWarnings("unchecked")
925 public final <E extends HtmlElement> List<E> getElementsByAttribute(
926 final String elementName,
927 final String attributeName,
928 final String attributeValue) {
929
930 final List<E> list = new ArrayList<>();
931 final String lowerCaseTagName = elementName.toLowerCase(Locale.ROOT);
932
933 for (final HtmlElement next : getHtmlElementDescendants()) {
934 if (next.getTagName().equals(lowerCaseTagName)) {
935 final String attValue = next.getAttribute(attributeName);
936 if (attValue.equals(attributeValue)) {
937 list.add((E) next);
938 }
939 }
940 }
941 return list;
942 }
943
944
945
946
947
948
949
950
951
952 public final HtmlElement appendChildIfNoneExists(final String tagName) {
953 final HtmlElement child;
954 final List<HtmlElement> children = getStaticElementsByTagName(tagName);
955 if (children.isEmpty()) {
956
957 child = (HtmlElement) ((HtmlPage) getPage()).createElement(tagName);
958 appendChild(child);
959 }
960 else {
961
962 child = children.get(0);
963 }
964 return child;
965 }
966
967
968
969
970
971
972
973 public final void removeChild(final String tagName, final int i) {
974 final List<HtmlElement> children = getStaticElementsByTagName(tagName);
975 if (i >= 0 && i < children.size()) {
976 children.get(i).remove();
977 }
978 }
979
980
981
982
983
984
985
986
987 public final boolean hasEventHandlers(final String eventName) {
988 if (getPage().getWebClient().isJavaScriptEngineEnabled()) {
989 final HtmlUnitScriptable jsObj = getScriptableObject();
990 if (jsObj instanceof EventTarget) {
991 return ((EventTarget) jsObj).hasEventHandlers(eventName);
992 }
993 }
994 return false;
995 }
996
997
998
999
1000
1001
1002
1003
1004
1005 public void addHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
1006 WebAssert.notNull("listener", listener);
1007 synchronized (attributeListeners_) {
1008 attributeListeners_.add(listener);
1009 }
1010 }
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020 public void removeHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
1021 WebAssert.notNull("listener", listener);
1022 synchronized (attributeListeners_) {
1023 attributeListeners_.remove(listener);
1024 }
1025 }
1026
1027
1028
1029
1030 @Override
1031 protected void checkChildHierarchy(final Node childNode) throws DOMException {
1032 if (!((childNode instanceof Element) || (childNode instanceof Text)
1033 || (childNode instanceof Comment) || (childNode instanceof ProcessingInstruction)
1034 || (childNode instanceof CDATASection) || (childNode instanceof EntityReference))) {
1035 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
1036 "The Element may not have a child of this type: " + childNode.getNodeType());
1037 }
1038 super.checkChildHierarchy(childNode);
1039 }
1040
1041
1042
1043
1044
1045
1046
1047 public void setOwningForm(final HtmlForm form) {
1048 owningForm_ = form;
1049 }
1050
1051
1052
1053
1054
1055 @Override
1056 protected boolean isAttributeCaseSensitive() {
1057 return false;
1058 }
1059
1060
1061
1062
1063
1064
1065
1066
1067 public final String getLangAttribute() {
1068 return getAttributeDirect("lang");
1069 }
1070
1071
1072
1073
1074
1075
1076
1077
1078 public final String getXmlLangAttribute() {
1079 return getAttribute("xml:lang");
1080 }
1081
1082
1083
1084
1085
1086
1087
1088
1089 public final String getTextDirectionAttribute() {
1090 return getAttributeDirect("dir");
1091 }
1092
1093
1094
1095
1096
1097
1098
1099
1100 public final String getOnClickAttribute() {
1101 return getAttributeDirect("onclick");
1102 }
1103
1104
1105
1106
1107
1108
1109
1110
1111 public final String getOnDblClickAttribute() {
1112 return getAttributeDirect("ondblclick");
1113 }
1114
1115
1116
1117
1118
1119
1120
1121
1122 public final String getOnMouseDownAttribute() {
1123 return getAttributeDirect("onmousedown");
1124 }
1125
1126
1127
1128
1129
1130
1131
1132
1133 public final String getOnMouseUpAttribute() {
1134 return getAttributeDirect("onmouseup");
1135 }
1136
1137
1138
1139
1140
1141
1142
1143
1144 public final String getOnMouseOverAttribute() {
1145 return getAttributeDirect("onmouseover");
1146 }
1147
1148
1149
1150
1151
1152
1153
1154
1155 public final String getOnMouseMoveAttribute() {
1156 return getAttributeDirect("onmousemove");
1157 }
1158
1159
1160
1161
1162
1163
1164
1165
1166 public final String getOnMouseOutAttribute() {
1167 return getAttributeDirect("onmouseout");
1168 }
1169
1170
1171
1172
1173
1174
1175
1176
1177 public final String getOnKeyPressAttribute() {
1178 return getAttributeDirect("onkeypress");
1179 }
1180
1181
1182
1183
1184
1185
1186
1187
1188 public final String getOnKeyDownAttribute() {
1189 return getAttributeDirect("onkeydown");
1190 }
1191
1192
1193
1194
1195
1196
1197
1198
1199 public final String getOnKeyUpAttribute() {
1200 return getAttributeDirect("onkeyup");
1201 }
1202
1203
1204
1205
1206 @Override
1207 public String getCanonicalXPath() {
1208 final DomNode parent = getParentNode();
1209 if (parent.getNodeType() == DOCUMENT_NODE) {
1210 return "/" + getNodeName();
1211 }
1212 return parent.getCanonicalXPath() + '/' + getXPathToken();
1213 }
1214
1215
1216
1217
1218 private String getXPathToken() {
1219 final DomNode parent = getParentNode();
1220 int total = 0;
1221 int nodeIndex = 0;
1222 for (final DomNode child : parent.getChildren()) {
1223 if (child.getNodeType() == ELEMENT_NODE && child.getNodeName().equals(getNodeName())) {
1224 total++;
1225 }
1226 if (child == this) {
1227 nodeIndex = total;
1228 }
1229 }
1230
1231 if (nodeIndex == 1 && total == 1) {
1232 return getNodeName();
1233 }
1234 return getNodeName() + '[' + nodeIndex + ']';
1235 }
1236
1237
1238
1239
1240 public boolean isHidden() {
1241 return ATTRIBUTE_NOT_DEFINED != getAttributeDirect(ATTRIBUTE_HIDDEN);
1242 }
1243
1244
1245
1246
1247
1248 public void setHidden(final String hidden) {
1249 if ("false".equalsIgnoreCase(hidden)) {
1250 removeAttribute(ATTRIBUTE_HIDDEN);
1251 }
1252
1253 if (StringUtils.isNotEmpty(hidden)) {
1254 setAttribute(ATTRIBUTE_HIDDEN, "");
1255 }
1256 }
1257
1258
1259
1260
1261
1262 public void setHidden(final boolean hidden) {
1263 if (hidden) {
1264 setAttribute("hidden", "");
1265 return;
1266 }
1267
1268 removeAttribute("hidden");
1269 }
1270
1271
1272
1273
1274
1275 @Override
1276 public boolean isDisplayed() {
1277 if (isHidden()) {
1278 return false;
1279 }
1280 return super.isDisplayed();
1281 }
1282
1283
1284
1285
1286
1287
1288
1289
1290 public DisplayStyle getDefaultStyleDisplay() {
1291 return DisplayStyle.BLOCK;
1292 }
1293
1294
1295
1296
1297
1298
1299
1300 protected final String getSrcAttributeNormalized() {
1301
1302
1303
1304 final String attrib = getAttributeDirect(SRC_ATTRIBUTE);
1305 if (ATTRIBUTE_NOT_DEFINED == attrib) {
1306 return attrib;
1307 }
1308
1309 return StringUtils.replaceChars(attrib, "\r\n", "");
1310 }
1311
1312
1313
1314
1315
1316
1317
1318 @Override
1319 protected void detach() {
1320 final SgmlPage page = getPage();
1321 if (!page.getWebClient().isJavaScriptEngineEnabled()) {
1322 super.detach();
1323 return;
1324 }
1325
1326 final HtmlUnitScriptable document = page.getScriptableObject();
1327
1328 if (document instanceof HTMLDocument) {
1329 final HTMLDocument doc = (HTMLDocument) document;
1330 final Object activeElement = doc.getActiveElement();
1331
1332 if (activeElement == getScriptableObject()) {
1333 if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1334 ((HtmlPage) page).setFocusedElement(null);
1335 }
1336 else {
1337 ((HtmlPage) page).setElementWithFocus(null);
1338 }
1339 }
1340 else {
1341 for (final DomNode child : getChildNodes()) {
1342 if (activeElement == child.getScriptableObject()) {
1343 if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1344 ((HtmlPage) page).setFocusedElement(null);
1345 }
1346 else {
1347 ((HtmlPage) page).setElementWithFocus(null);
1348 }
1349
1350 break;
1351 }
1352 }
1353 }
1354 }
1355 super.detach();
1356 }
1357
1358
1359
1360
1361 @Override
1362 public boolean handles(final Event event) {
1363 if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
1364 return this instanceof SubmittableElement || getTabIndex() != null;
1365 }
1366
1367 if (isDisabledElementAndDisabled()) {
1368 return false;
1369 }
1370 return super.handles(event);
1371 }
1372
1373
1374
1375
1376
1377 protected boolean isShiftPressed() {
1378 return shiftPressed_;
1379 }
1380
1381
1382
1383
1384
1385 public boolean isCtrlPressed() {
1386 return ctrlPressed_;
1387 }
1388
1389
1390
1391
1392
1393 public boolean isAltPressed() {
1394 return altPressed_;
1395 }
1396
1397
1398
1399
1400
1401 public boolean isValid() {
1402 return !isRequiredSupported()
1403 || ATTRIBUTE_NOT_DEFINED == getAttributeDirect(ATTRIBUTE_REQUIRED)
1404 || !getAttributeDirect(VALUE_ATTRIBUTE).isEmpty();
1405 }
1406
1407
1408
1409
1410
1411 protected boolean isRequiredSupported() {
1412 return false;
1413 }
1414
1415
1416
1417
1418 public boolean isRequired() {
1419 return isRequiredSupported() && hasAttribute(ATTRIBUTE_REQUIRED);
1420 }
1421
1422
1423
1424
1425 public boolean isOptional() {
1426 return isRequiredSupported() && !hasAttribute(ATTRIBUTE_REQUIRED);
1427 }
1428
1429
1430
1431
1432
1433 public void setRequired(final boolean required) {
1434 if (isRequiredSupported()) {
1435 if (required) {
1436 setAttribute(ATTRIBUTE_REQUIRED, ATTRIBUTE_REQUIRED);
1437 }
1438 else {
1439 removeAttribute(ATTRIBUTE_REQUIRED);
1440 }
1441 }
1442 }
1443
1444
1445
1446
1447
1448
1449
1450 public HtmlElement getOffsetParentInternal(final boolean returnNullIfFixed) {
1451 if (getParentNode() == null) {
1452 return null;
1453 }
1454
1455 final WebWindow webWindow = getPage().getEnclosingWindow();
1456 final ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1457 final String position = style.getPositionWithInheritance();
1458
1459 if (returnNullIfFixed && FIXED.equals(position)) {
1460 return null;
1461 }
1462
1463 final boolean staticPos = STATIC.equals(position);
1464
1465 DomNode currentElement = this;
1466 while (currentElement != null) {
1467
1468 final DomNode parentNode = currentElement.getParentNode();
1469 if (parentNode instanceof HtmlBody
1470 || (staticPos && parentNode instanceof HtmlTableDataCell)
1471 || (staticPos && parentNode instanceof HtmlTable)) {
1472 return (HtmlElement) parentNode;
1473 }
1474
1475 if (parentNode instanceof HtmlElement) {
1476 final ComputedCssStyleDeclaration parentStyle =
1477 webWindow.getComputedStyle((HtmlElement) parentNode, null);
1478 final String parentPosition = parentStyle.getPositionWithInheritance();
1479 if (!STATIC.equals(parentPosition)) {
1480 return (HtmlElement) parentNode;
1481 }
1482 }
1483
1484 currentElement = currentElement.getParentNode();
1485 }
1486
1487 return null;
1488 }
1489
1490
1491
1492
1493
1494 public int getOffsetTop() {
1495 if (this instanceof HtmlBody) {
1496 return 0;
1497 }
1498
1499 int top = 0;
1500
1501
1502 final WebWindow webWindow = getPage().getEnclosingWindow();
1503 ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1504 top += style.getTop(true, false, false);
1505
1506
1507 final String position = style.getPositionWithInheritance();
1508 if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
1509 return top;
1510 }
1511
1512 final HtmlElement offsetParent = getOffsetParentInternal(false);
1513
1514
1515 DomNode parentNode = getParentNode();
1516 while (parentNode != null && parentNode != offsetParent) {
1517 if (parentNode instanceof HtmlElement) {
1518 style = webWindow.getComputedStyle((HtmlElement) parentNode, null);
1519 top += style.getTop(false, true, true);
1520 }
1521 parentNode = parentNode.getParentNode();
1522 }
1523
1524 if (offsetParent != null) {
1525 style = webWindow.getComputedStyle(this, null);
1526 final boolean thisElementHasTopMargin = style.getMarginTopValue() != 0;
1527
1528 style = webWindow.getComputedStyle(offsetParent, null);
1529 if (!thisElementHasTopMargin) {
1530 top += style.getMarginTopValue();
1531 }
1532 top += style.getPaddingTopValue();
1533 }
1534
1535 return top;
1536 }
1537
1538
1539
1540
1541
1542 public int getOffsetLeft() {
1543 if (this instanceof HtmlBody) {
1544 return 0;
1545 }
1546
1547 int left = 0;
1548
1549
1550 final WebWindow webWindow = getPage().getEnclosingWindow();
1551 ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1552 left += style.getLeft(true, false, false);
1553
1554
1555 final String position = style.getPositionWithInheritance();
1556 if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
1557 return left;
1558 }
1559
1560 final HtmlElement offsetParent = getOffsetParentInternal(false);
1561
1562 DomNode parentNode = getParentNode();
1563 while (parentNode != null && parentNode != offsetParent) {
1564 if (parentNode instanceof HtmlElement) {
1565 style = webWindow.getComputedStyle((HtmlElement) parentNode, null);
1566 left += style.getLeft(true, true, true);
1567 }
1568 parentNode = parentNode.getParentNode();
1569 }
1570
1571 if (offsetParent != null) {
1572 style = webWindow.getComputedStyle(offsetParent, null);
1573 left += style.getMarginLeftValue();
1574 left += style.getPaddingLeftValue();
1575 }
1576
1577 return left;
1578 }
1579
1580
1581
1582
1583
1584 public int getPosX() {
1585 int cumulativeOffset = 0;
1586 final WebWindow webWindow = getPage().getEnclosingWindow();
1587
1588 HtmlElement element = this;
1589 while (element != null) {
1590 cumulativeOffset += element.getOffsetLeft();
1591 if (element != this) {
1592 final ComputedCssStyleDeclaration style =
1593 webWindow.getComputedStyle(element, null);
1594 cumulativeOffset += style.getBorderLeftValue();
1595 }
1596 element = element.getOffsetParentInternal(false);
1597 }
1598
1599 return cumulativeOffset;
1600 }
1601
1602
1603
1604
1605
1606 public int getPosY() {
1607 int cumulativeOffset = 0;
1608 final WebWindow webWindow = getPage().getEnclosingWindow();
1609
1610 HtmlElement element = this;
1611 while (element != null) {
1612 cumulativeOffset += element.getOffsetTop();
1613 if (element != this) {
1614 final ComputedCssStyleDeclaration style =
1615 webWindow.getComputedStyle(element, null);
1616 cumulativeOffset += style.getBorderTopValue();
1617 }
1618 element = element.getOffsetParentInternal(false);
1619 }
1620
1621 return cumulativeOffset;
1622 }
1623
1624
1625
1626
1627 @Override
1628 public DomNode cloneNode(final boolean deep) {
1629 final HtmlElement newNode = (HtmlElement) super.cloneNode(deep);
1630 if (!deep) {
1631 synchronized (attributeListeners_) {
1632 newNode.attributeListeners_.clear();
1633 newNode.attributeListeners_.addAll(attributeListeners_);
1634 }
1635 }
1636
1637 return newNode;
1638 }
1639 }