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