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 String getHidden() {
1241 return getAttributeDirect(ATTRIBUTE_HIDDEN);
1242 }
1243
1244
1245
1246
1247 public boolean isHidden() {
1248 return ATTRIBUTE_NOT_DEFINED != getAttributeDirect(ATTRIBUTE_HIDDEN);
1249 }
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259 public void setHidden(final String hidden) {
1260 if ("until-found".equalsIgnoreCase(hidden)) {
1261 setAttribute(ATTRIBUTE_HIDDEN, "until-found");
1262 return;
1263 }
1264
1265 if (org.htmlunit.util.StringUtils.isEmptyString(hidden)) {
1266 removeAttribute(ATTRIBUTE_HIDDEN);
1267 return;
1268 }
1269
1270 setAttribute(ATTRIBUTE_HIDDEN, "");
1271 }
1272
1273
1274
1275
1276
1277 public void setHidden(final boolean hidden) {
1278 if (hidden) {
1279 setAttribute(ATTRIBUTE_HIDDEN, "");
1280 return;
1281 }
1282
1283 removeAttribute(ATTRIBUTE_HIDDEN);
1284 }
1285
1286
1287
1288
1289
1290 @Override
1291 public boolean isDisplayed() {
1292 if (isHidden()) {
1293 return false;
1294 }
1295 return super.isDisplayed();
1296 }
1297
1298
1299
1300
1301
1302
1303
1304
1305 public DisplayStyle getDefaultStyleDisplay() {
1306 return DisplayStyle.BLOCK;
1307 }
1308
1309
1310
1311
1312
1313
1314
1315 protected final String getSrcAttributeNormalized() {
1316
1317
1318
1319 final String attrib = getAttributeDirect(SRC_ATTRIBUTE);
1320 if (ATTRIBUTE_NOT_DEFINED == attrib) {
1321 return attrib;
1322 }
1323
1324 return StringUtils.replaceChars(attrib, "\r\n", "");
1325 }
1326
1327
1328
1329
1330
1331
1332
1333 @Override
1334 protected void detach() {
1335 final SgmlPage page = getPage();
1336 if (!page.getWebClient().isJavaScriptEngineEnabled()) {
1337 super.detach();
1338 return;
1339 }
1340
1341 final HtmlUnitScriptable document = page.getScriptableObject();
1342
1343 if (document instanceof HTMLDocument) {
1344 final HTMLDocument doc = (HTMLDocument) document;
1345 final Object activeElement = doc.getActiveElement();
1346
1347 if (activeElement == getScriptableObject()) {
1348 if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1349 ((HtmlPage) page).setFocusedElement(null);
1350 }
1351 else {
1352 ((HtmlPage) page).setElementWithFocus(null);
1353 }
1354 }
1355 else {
1356 for (final DomNode child : getChildNodes()) {
1357 if (activeElement == child.getScriptableObject()) {
1358 if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1359 ((HtmlPage) page).setFocusedElement(null);
1360 }
1361 else {
1362 ((HtmlPage) page).setElementWithFocus(null);
1363 }
1364
1365 break;
1366 }
1367 }
1368 }
1369 }
1370 super.detach();
1371 }
1372
1373
1374
1375
1376 @Override
1377 public boolean handles(final Event event) {
1378 if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
1379 return this instanceof SubmittableElement || getTabIndex() != null;
1380 }
1381
1382 if (isDisabledElementAndDisabled()) {
1383 return false;
1384 }
1385 return super.handles(event);
1386 }
1387
1388
1389
1390
1391
1392 protected boolean isShiftPressed() {
1393 return shiftPressed_;
1394 }
1395
1396
1397
1398
1399
1400 public boolean isCtrlPressed() {
1401 return ctrlPressed_;
1402 }
1403
1404
1405
1406
1407
1408 public boolean isAltPressed() {
1409 return altPressed_;
1410 }
1411
1412
1413
1414
1415
1416 public boolean isValid() {
1417 return !isRequiredSupported()
1418 || ATTRIBUTE_NOT_DEFINED == getAttributeDirect(ATTRIBUTE_REQUIRED)
1419 || !getAttributeDirect(VALUE_ATTRIBUTE).isEmpty();
1420 }
1421
1422
1423
1424
1425
1426 protected boolean isRequiredSupported() {
1427 return false;
1428 }
1429
1430
1431
1432
1433 public boolean isRequired() {
1434 return isRequiredSupported() && hasAttribute(ATTRIBUTE_REQUIRED);
1435 }
1436
1437
1438
1439
1440 public boolean isOptional() {
1441 return isRequiredSupported() && !hasAttribute(ATTRIBUTE_REQUIRED);
1442 }
1443
1444
1445
1446
1447
1448 public void setRequired(final boolean required) {
1449 if (isRequiredSupported()) {
1450 if (required) {
1451 setAttribute(ATTRIBUTE_REQUIRED, ATTRIBUTE_REQUIRED);
1452 }
1453 else {
1454 removeAttribute(ATTRIBUTE_REQUIRED);
1455 }
1456 }
1457 }
1458
1459
1460
1461
1462
1463
1464
1465 public HtmlElement getOffsetParentInternal(final boolean returnNullIfFixed) {
1466 if (getParentNode() == null) {
1467 return null;
1468 }
1469
1470 final WebWindow webWindow = getPage().getEnclosingWindow();
1471 final ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1472 final String position = style.getPositionWithInheritance();
1473
1474 if (returnNullIfFixed && FIXED.equals(position)) {
1475 return null;
1476 }
1477
1478 final boolean staticPos = STATIC.equals(position);
1479
1480 DomNode currentElement = this;
1481 while (currentElement != null) {
1482
1483 final DomNode parentNode = currentElement.getParentNode();
1484 if (parentNode instanceof HtmlBody
1485 || (staticPos && parentNode instanceof HtmlTableDataCell)
1486 || (staticPos && parentNode instanceof HtmlTable)) {
1487 return (HtmlElement) parentNode;
1488 }
1489
1490 if (parentNode instanceof HtmlElement) {
1491 final ComputedCssStyleDeclaration parentStyle =
1492 webWindow.getComputedStyle((HtmlElement) parentNode, null);
1493 final String parentPosition = parentStyle.getPositionWithInheritance();
1494 if (!STATIC.equals(parentPosition)) {
1495 return (HtmlElement) parentNode;
1496 }
1497 }
1498
1499 currentElement = currentElement.getParentNode();
1500 }
1501
1502 return null;
1503 }
1504
1505
1506
1507
1508
1509 public int getOffsetTop() {
1510 if (this instanceof HtmlBody) {
1511 return 0;
1512 }
1513
1514 int top = 0;
1515
1516
1517 final WebWindow webWindow = getPage().getEnclosingWindow();
1518 ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1519 top += style.getTop(true, false, false);
1520
1521
1522 final String position = style.getPositionWithInheritance();
1523 if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
1524 return top;
1525 }
1526
1527 final HtmlElement offsetParent = getOffsetParentInternal(false);
1528
1529
1530 DomNode parentNode = getParentNode();
1531 while (parentNode != null && parentNode != offsetParent) {
1532 if (parentNode instanceof HtmlElement) {
1533 style = webWindow.getComputedStyle((HtmlElement) parentNode, null);
1534 top += style.getTop(false, true, true);
1535 }
1536 parentNode = parentNode.getParentNode();
1537 }
1538
1539 if (offsetParent != null) {
1540 style = webWindow.getComputedStyle(this, null);
1541 final boolean thisElementHasTopMargin = style.getMarginTopValue() != 0;
1542
1543 style = webWindow.getComputedStyle(offsetParent, null);
1544 if (!thisElementHasTopMargin) {
1545 top += style.getMarginTopValue();
1546 }
1547 top += style.getPaddingTopValue();
1548 }
1549
1550 return top;
1551 }
1552
1553
1554
1555
1556
1557 public int getOffsetLeft() {
1558 if (this instanceof HtmlBody) {
1559 return 0;
1560 }
1561
1562 int left = 0;
1563
1564
1565 final WebWindow webWindow = getPage().getEnclosingWindow();
1566 ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1567 left += style.getLeft(true, false, false);
1568
1569
1570 final String position = style.getPositionWithInheritance();
1571 if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
1572 return left;
1573 }
1574
1575 final HtmlElement offsetParent = getOffsetParentInternal(false);
1576
1577 DomNode parentNode = getParentNode();
1578 while (parentNode != null && parentNode != offsetParent) {
1579 if (parentNode instanceof HtmlElement) {
1580 style = webWindow.getComputedStyle((HtmlElement) parentNode, null);
1581 left += style.getLeft(true, true, true);
1582 }
1583 parentNode = parentNode.getParentNode();
1584 }
1585
1586 if (offsetParent != null) {
1587 style = webWindow.getComputedStyle(offsetParent, null);
1588 left += style.getMarginLeftValue();
1589 left += style.getPaddingLeftValue();
1590 }
1591
1592 return left;
1593 }
1594
1595
1596
1597
1598
1599 public int getPosX() {
1600 int cumulativeOffset = 0;
1601 final WebWindow webWindow = getPage().getEnclosingWindow();
1602
1603 HtmlElement element = this;
1604 while (element != null) {
1605 cumulativeOffset += element.getOffsetLeft();
1606 if (element != this) {
1607 final ComputedCssStyleDeclaration style =
1608 webWindow.getComputedStyle(element, null);
1609 cumulativeOffset += style.getBorderLeftValue();
1610 }
1611 element = element.getOffsetParentInternal(false);
1612 }
1613
1614 return cumulativeOffset;
1615 }
1616
1617
1618
1619
1620
1621 public int getPosY() {
1622 int cumulativeOffset = 0;
1623 final WebWindow webWindow = getPage().getEnclosingWindow();
1624
1625 HtmlElement element = this;
1626 while (element != null) {
1627 cumulativeOffset += element.getOffsetTop();
1628 if (element != this) {
1629 final ComputedCssStyleDeclaration style =
1630 webWindow.getComputedStyle(element, null);
1631 cumulativeOffset += style.getBorderTopValue();
1632 }
1633 element = element.getOffsetParentInternal(false);
1634 }
1635
1636 return cumulativeOffset;
1637 }
1638
1639
1640
1641
1642 @Override
1643 public DomNode cloneNode(final boolean deep) {
1644 final HtmlElement newNode = (HtmlElement) super.cloneNode(deep);
1645 if (!deep) {
1646 synchronized (attributeListeners_) {
1647 newNode.attributeListeners_.clear();
1648 newNode.attributeListeners_.addAll(attributeListeners_);
1649 }
1650 }
1651
1652 return newNode;
1653 }
1654 }