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
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 htmlElement) {
264 notifyAttributeChangeListeners(event, htmlElement, 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 element) {
378 element.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 element) {
396 element.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 element) {
419 element.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 element && currentNode.getNodeName().equals(tagNameLC)) {
475 return element;
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 form) {
491 return form;
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 HtmlSelectableTextInput
596 || this instanceof HtmlTextArea) {
597 fireEvent(new KeyboardEvent(this, Event.TYPE_INPUT, c,
598 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_));
599 }
600
601 HtmlElement eventSource = this;
602 if (!isAttachedToPage()) {
603 eventSource = page.getBody();
604 }
605
606 if (eventSource != null) {
607 final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, c,
608 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
609 eventSource.fireEvent(keyUp);
610
611 if (isShiftNeeded) {
612 final Event shiftUp = new KeyboardEvent(this, Event.TYPE_KEY_UP,
613 KeyboardEvent.DOM_VK_SHIFT,
614 false, ctrlPressed_, altPressed_);
615 eventSource.fireEvent(shiftUp);
616 }
617 }
618
619 final HtmlForm form = getEnclosingForm();
620 if (form != null && c == '\n' && isSubmittableByEnter()) {
621 for (final DomElement descendant : form.getDomElementDescendants()) {
622 if (descendant instanceof HtmlSubmitInput) {
623 return descendant.click();
624 }
625 }
626
627 form.submit((SubmittableElement) this);
628
629 if (webClient.isJavaScriptEnabled()) {
630 webClient.getJavaScriptEngine().processPostponedActions();
631 }
632 }
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 textInput) {
671 textInput.setSelectionStart(textInput.getText().length());
672 }
673 else {
674 final DomText domText = getDoTypeNode();
675 if (domText != null) {
676 domText.moveSelectionToEnd();
677 }
678 }
679 }
680
681 final int size = keys.size();
682 for (int i = 0; i < size; i++) {
683 final Object[] entry = keys.get(i);
684 if (entry.length == 1) {
685 type((char) entry[0], i == keys.size() - 1);
686 }
687 else {
688 final int key = (int) entry[0];
689 final boolean pressed = (boolean) entry[1];
690 switch (key) {
691 case KeyboardEvent.DOM_VK_SHIFT:
692 shiftPressed_ = pressed;
693 break;
694
695 case KeyboardEvent.DOM_VK_CONTROL:
696 ctrlPressed_ = pressed;
697 break;
698
699 case KeyboardEvent.DOM_VK_ALT:
700 altPressed_ = pressed;
701 break;
702
703 default:
704 }
705 if (pressed) {
706 boolean keyPress = true;
707 boolean keyUp = true;
708 switch (key) {
709 case KeyboardEvent.DOM_VK_SHIFT:
710 case KeyboardEvent.DOM_VK_CONTROL:
711 case KeyboardEvent.DOM_VK_ALT:
712 keyPress = false;
713 keyUp = false;
714 break;
715
716 default:
717 }
718 page = type(key, true, keyPress, keyUp, i == keys.size() - 1);
719 }
720 else {
721 page = type(key, false, false, true, i == keys.size() - 1);
722 }
723 }
724 }
725
726 return page;
727 }
728
729 private Page type(final int keyCode,
730 final boolean fireKeyDown, final boolean fireKeyPress, final boolean fireKeyUp,
731 final boolean lastType) {
732 if (isDisabledElementAndDisabled()) {
733 return getPage();
734 }
735
736 final HtmlPage page = (HtmlPage) getPage();
737 if (page.getFocusedElement() != this) {
738 focus();
739 }
740
741 final Event keyDown;
742 final ScriptResult keyDownResult;
743 if (fireKeyDown) {
744 keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, keyCode, shiftPressed_, ctrlPressed_, altPressed_);
745 keyDownResult = fireEvent(keyDown);
746 }
747 else {
748 keyDown = null;
749 keyDownResult = null;
750 }
751
752 final BrowserVersion browserVersion = page.getWebClient().getBrowserVersion();
753
754 final Event keyPress;
755 final ScriptResult keyPressResult;
756 if (fireKeyPress && browserVersion.hasFeature(KEYBOARD_EVENT_SPECIAL_KEYPRESS)) {
757 keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, keyCode,
758 shiftPressed_, ctrlPressed_, altPressed_);
759
760 keyPressResult = fireEvent(keyPress);
761 }
762 else {
763 keyPress = null;
764 keyPressResult = null;
765 }
766
767 if (keyDown != null && !keyDown.isAborted(keyDownResult)
768 && (keyPress == null || !keyPress.isAborted(keyPressResult))) {
769 doType(keyCode, lastType);
770 }
771
772 if (this instanceof HtmlTextInput
773 || this instanceof HtmlTextArea
774 || this instanceof HtmlTelInput
775 || this instanceof HtmlNumberInput
776 || this instanceof HtmlSearchInput
777 || this instanceof HtmlPasswordInput) {
778 final Event input = new KeyboardEvent(this, Event.TYPE_INPUT, keyCode,
779 shiftPressed_, ctrlPressed_, altPressed_);
780 fireEvent(input);
781 }
782
783 if (fireKeyUp) {
784 final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, keyCode,
785 shiftPressed_, ctrlPressed_, altPressed_);
786 fireEvent(keyUp);
787 }
788
789
790
791
792
793
794
795
796
797
798
799
800
801 return page.getWebClient().getCurrentWindow().getEnclosedPage();
802 }
803
804
805
806
807
808
809 protected void doType(final char c, final boolean lastType) {
810 final DomText domText = getDoTypeNode();
811 if (domText != null) {
812 domText.doType(c, this, lastType);
813 }
814 }
815
816
817
818
819
820
821
822
823
824 protected void doType(final int keyCode, final boolean lastType) {
825 final DomText domText = getDoTypeNode();
826 if (domText != null) {
827 domText.doType(keyCode, this, lastType);
828 }
829 }
830
831
832
833
834
835 private DomText getDoTypeNode() {
836 final HTMLElement scriptElement = getScriptableObject();
837 if (scriptElement.isIsContentEditable()
838 || "on".equals(((Document) scriptElement.getOwnerDocument()).getDesignMode())) {
839
840 DomNodeList<DomNode> children = getChildNodes();
841 while (!children.isEmpty()) {
842 final DomNode lastChild = children.get(children.size() - 1);
843 if (lastChild instanceof DomText text) {
844 return text;
845 }
846 children = lastChild.getChildNodes();
847 }
848
849 final DomText domText = new DomText(getPage(), "");
850 appendChild(domText);
851 return domText;
852 }
853 return null;
854 }
855
856
857
858
859
860
861 protected void typeDone(final String newValue, final boolean notifyAttributeChangeListeners) {
862
863 }
864
865
866
867
868
869
870 protected boolean acceptChar(final char c) {
871
872
873 return (c < '\uE000' || c > '\uF8FF')
874 && (c == ' ' || c == '\t' || c == '\u3000' || c == '\u2006' || !Character.isWhitespace(c));
875 }
876
877
878
879
880
881
882 protected boolean isSubmittableByEnter() {
883 return false;
884 }
885
886
887
888
889
890
891
892
893
894
895
896
897 public final <E extends HtmlElement> E getOneHtmlElementByAttribute(final String elementName,
898 final String attributeName,
899 final String attributeValue) throws ElementNotFoundException {
900
901 WebAssert.notNull("elementName", elementName);
902 WebAssert.notNull("attributeName", attributeName);
903 WebAssert.notNull("attributeValue", attributeValue);
904
905 final List<E> list = getElementsByAttribute(elementName, attributeName, attributeValue);
906
907 if (list.isEmpty()) {
908 throw new ElementNotFoundException(elementName, attributeName, attributeValue);
909 }
910
911 return list.get(0);
912 }
913
914
915
916
917
918
919
920
921
922
923 @SuppressWarnings("unchecked")
924 public final <E extends HtmlElement> List<E> getElementsByAttribute(
925 final String elementName,
926 final String attributeName,
927 final String attributeValue) {
928
929 final List<E> list = new ArrayList<>();
930 final String lowerCaseTagName = elementName.toLowerCase(Locale.ROOT);
931
932 for (final HtmlElement next : getHtmlElementDescendants()) {
933 if (next.getTagName().equals(lowerCaseTagName)) {
934 final String attValue = next.getAttribute(attributeName);
935 if (attValue.equals(attributeValue)) {
936 list.add((E) next);
937 }
938 }
939 }
940 return list;
941 }
942
943
944
945
946
947
948
949
950
951 public final HtmlElement appendChildIfNoneExists(final String tagName) {
952 final HtmlElement child;
953 final List<HtmlElement> children = getStaticElementsByTagName(tagName);
954 if (children.isEmpty()) {
955
956 child = (HtmlElement) ((HtmlPage) getPage()).createElement(tagName);
957 appendChild(child);
958 }
959 else {
960
961 child = children.get(0);
962 }
963 return child;
964 }
965
966
967
968
969
970
971
972 public final void removeChild(final String tagName, final int i) {
973 final List<HtmlElement> children = getStaticElementsByTagName(tagName);
974 if (i >= 0 && i < children.size()) {
975 children.get(i).remove();
976 }
977 }
978
979
980
981
982
983
984
985
986 public final boolean hasEventHandlers(final String eventName) {
987 if (getPage().getWebClient().isJavaScriptEngineEnabled()) {
988 final HtmlUnitScriptable jsObj = getScriptableObject();
989 if (jsObj instanceof EventTarget target) {
990 return target.hasEventHandlers(eventName);
991 }
992 }
993 return false;
994 }
995
996
997
998
999
1000
1001
1002
1003
1004 public void addHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
1005 WebAssert.notNull("listener", listener);
1006 synchronized (attributeListeners_) {
1007 attributeListeners_.add(listener);
1008 }
1009 }
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019 public void removeHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
1020 WebAssert.notNull("listener", listener);
1021 synchronized (attributeListeners_) {
1022 attributeListeners_.remove(listener);
1023 }
1024 }
1025
1026
1027
1028
1029 @Override
1030 protected void checkChildHierarchy(final Node childNode) throws DOMException {
1031 if (!((childNode instanceof Element) || (childNode instanceof Text)
1032 || (childNode instanceof Comment) || (childNode instanceof ProcessingInstruction)
1033 || (childNode instanceof CDATASection) || (childNode instanceof EntityReference))) {
1034 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
1035 "The Element may not have a child of this type: " + childNode.getNodeType());
1036 }
1037 super.checkChildHierarchy(childNode);
1038 }
1039
1040
1041
1042
1043
1044
1045
1046 public void setOwningForm(final HtmlForm form) {
1047 owningForm_ = form;
1048 }
1049
1050
1051
1052
1053
1054 @Override
1055 protected boolean isAttributeCaseSensitive() {
1056 return false;
1057 }
1058
1059
1060
1061
1062
1063
1064
1065
1066 public final String getLangAttribute() {
1067 return getAttributeDirect("lang");
1068 }
1069
1070
1071
1072
1073
1074
1075
1076
1077 public final String getXmlLangAttribute() {
1078 return getAttribute("xml:lang");
1079 }
1080
1081
1082
1083
1084
1085
1086
1087
1088 public final String getTextDirectionAttribute() {
1089 return getAttributeDirect("dir");
1090 }
1091
1092
1093
1094
1095
1096
1097
1098
1099 public final String getOnClickAttribute() {
1100 return getAttributeDirect("onclick");
1101 }
1102
1103
1104
1105
1106
1107
1108
1109
1110 public final String getOnDblClickAttribute() {
1111 return getAttributeDirect("ondblclick");
1112 }
1113
1114
1115
1116
1117
1118
1119
1120
1121 public final String getOnMouseDownAttribute() {
1122 return getAttributeDirect("onmousedown");
1123 }
1124
1125
1126
1127
1128
1129
1130
1131
1132 public final String getOnMouseUpAttribute() {
1133 return getAttributeDirect("onmouseup");
1134 }
1135
1136
1137
1138
1139
1140
1141
1142
1143 public final String getOnMouseOverAttribute() {
1144 return getAttributeDirect("onmouseover");
1145 }
1146
1147
1148
1149
1150
1151
1152
1153
1154 public final String getOnMouseMoveAttribute() {
1155 return getAttributeDirect("onmousemove");
1156 }
1157
1158
1159
1160
1161
1162
1163
1164
1165 public final String getOnMouseOutAttribute() {
1166 return getAttributeDirect("onmouseout");
1167 }
1168
1169
1170
1171
1172
1173
1174
1175
1176 public final String getOnKeyPressAttribute() {
1177 return getAttributeDirect("onkeypress");
1178 }
1179
1180
1181
1182
1183
1184
1185
1186
1187 public final String getOnKeyDownAttribute() {
1188 return getAttributeDirect("onkeydown");
1189 }
1190
1191
1192
1193
1194
1195
1196
1197
1198 public final String getOnKeyUpAttribute() {
1199 return getAttributeDirect("onkeyup");
1200 }
1201
1202
1203
1204
1205 @Override
1206 public String getCanonicalXPath() {
1207 final DomNode parent = getParentNode();
1208 if (parent.getNodeType() == DOCUMENT_NODE) {
1209 return "/" + getNodeName();
1210 }
1211 return parent.getCanonicalXPath() + '/' + getXPathToken();
1212 }
1213
1214
1215
1216
1217 private String getXPathToken() {
1218 final DomNode parent = getParentNode();
1219 int total = 0;
1220 int nodeIndex = 0;
1221 for (final DomNode child : parent.getChildren()) {
1222 if (child.getNodeType() == ELEMENT_NODE && child.getNodeName().equals(getNodeName())) {
1223 total++;
1224 }
1225 if (child == this) {
1226 nodeIndex = total;
1227 }
1228 }
1229
1230 if (nodeIndex == 1 && total == 1) {
1231 return getNodeName();
1232 }
1233 return getNodeName() + '[' + nodeIndex + ']';
1234 }
1235
1236
1237
1238
1239 public String getHidden() {
1240 return getAttributeDirect(ATTRIBUTE_HIDDEN);
1241 }
1242
1243
1244
1245
1246 public boolean isHidden() {
1247 return ATTRIBUTE_NOT_DEFINED != getAttributeDirect(ATTRIBUTE_HIDDEN);
1248 }
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258 public void setHidden(final String hidden) {
1259 if ("until-found".equalsIgnoreCase(hidden)) {
1260 setAttribute(ATTRIBUTE_HIDDEN, "until-found");
1261 return;
1262 }
1263
1264 if (StringUtils.isEmptyString(hidden)) {
1265 removeAttribute(ATTRIBUTE_HIDDEN);
1266 return;
1267 }
1268
1269 setAttribute(ATTRIBUTE_HIDDEN, "");
1270 }
1271
1272
1273
1274
1275
1276 public void setHidden(final boolean hidden) {
1277 if (hidden) {
1278 setAttribute(ATTRIBUTE_HIDDEN, "");
1279 return;
1280 }
1281
1282 removeAttribute(ATTRIBUTE_HIDDEN);
1283 }
1284
1285
1286
1287
1288
1289 @Override
1290 public boolean isDisplayed() {
1291 if (isHidden()) {
1292 return false;
1293 }
1294 return super.isDisplayed();
1295 }
1296
1297
1298
1299
1300
1301
1302
1303
1304 public DisplayStyle getDefaultStyleDisplay() {
1305 return DisplayStyle.BLOCK;
1306 }
1307
1308
1309
1310
1311
1312
1313
1314 protected final String getSrcAttributeNormalized() {
1315 final String attrib = getAttributeDirect(SRC_ATTRIBUTE);
1316 if (ATTRIBUTE_NOT_DEFINED == attrib) {
1317 return attrib;
1318 }
1319
1320 return StringUtils.replaceChars(attrib, "\r\n", "");
1321 }
1322
1323
1324
1325
1326
1327
1328
1329 @Override
1330 protected void detach() {
1331 final SgmlPage page = getPage();
1332 if (!page.getWebClient().isJavaScriptEngineEnabled()) {
1333 super.detach();
1334 return;
1335 }
1336
1337 final HtmlUnitScriptable document = page.getScriptableObject();
1338
1339 if (document instanceof HTMLDocument doc) {
1340 final Object activeElement = doc.getActiveElement();
1341
1342 if (activeElement == 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 else {
1351 for (final DomNode child : getChildNodes()) {
1352 if (activeElement == child.getScriptableObject()) {
1353 if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1354 ((HtmlPage) page).setFocusedElement(null);
1355 }
1356 else {
1357 ((HtmlPage) page).setElementWithFocus(null);
1358 }
1359
1360 break;
1361 }
1362 }
1363 }
1364 }
1365 super.detach();
1366 }
1367
1368
1369
1370
1371 @Override
1372 public boolean handles(final Event event) {
1373 if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
1374 return this instanceof SubmittableElement || getTabIndex() != null;
1375 }
1376
1377 if (isDisabledElementAndDisabled()) {
1378 return false;
1379 }
1380 return super.handles(event);
1381 }
1382
1383
1384
1385
1386
1387 protected boolean isShiftPressed() {
1388 return shiftPressed_;
1389 }
1390
1391
1392
1393
1394
1395 public boolean isCtrlPressed() {
1396 return ctrlPressed_;
1397 }
1398
1399
1400
1401
1402
1403 public boolean isAltPressed() {
1404 return altPressed_;
1405 }
1406
1407
1408
1409
1410
1411 public boolean isValid() {
1412 return !isRequiredSupported()
1413 || ATTRIBUTE_NOT_DEFINED == getAttributeDirect(ATTRIBUTE_REQUIRED)
1414 || !getAttributeDirect(VALUE_ATTRIBUTE).isEmpty();
1415 }
1416
1417
1418
1419
1420
1421 protected boolean isRequiredSupported() {
1422 return false;
1423 }
1424
1425
1426
1427
1428 public boolean isRequired() {
1429 return isRequiredSupported() && hasAttribute(ATTRIBUTE_REQUIRED);
1430 }
1431
1432
1433
1434
1435 public boolean isOptional() {
1436 return isRequiredSupported() && !hasAttribute(ATTRIBUTE_REQUIRED);
1437 }
1438
1439
1440
1441
1442
1443 public void setRequired(final boolean required) {
1444 if (isRequiredSupported()) {
1445 if (required) {
1446 setAttribute(ATTRIBUTE_REQUIRED, ATTRIBUTE_REQUIRED);
1447 }
1448 else {
1449 removeAttribute(ATTRIBUTE_REQUIRED);
1450 }
1451 }
1452 }
1453
1454
1455
1456
1457
1458
1459
1460 public HtmlElement getOffsetParentInternal(final boolean returnNullIfFixed) {
1461 if (getParentNode() == null) {
1462 return null;
1463 }
1464
1465 final WebWindow webWindow = getPage().getEnclosingWindow();
1466 final ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1467 final String position = style.getPositionWithInheritance();
1468
1469 if (returnNullIfFixed && FIXED.equals(position)) {
1470 return null;
1471 }
1472
1473 final boolean staticPos = STATIC.equals(position);
1474
1475 DomNode currentElement = this;
1476 while (currentElement != null) {
1477
1478 final DomNode parentNode = currentElement.getParentNode();
1479 if (parentNode instanceof HtmlBody
1480 || (staticPos && parentNode instanceof HtmlTableDataCell)
1481 || (staticPos && parentNode instanceof HtmlTable)) {
1482 return (HtmlElement) parentNode;
1483 }
1484
1485 if (parentNode instanceof HtmlElement element) {
1486 final ComputedCssStyleDeclaration parentStyle =
1487 webWindow.getComputedStyle(element, null);
1488 final String parentPosition = parentStyle.getPositionWithInheritance();
1489 if (!STATIC.equals(parentPosition)) {
1490 return element;
1491 }
1492 }
1493
1494 currentElement = currentElement.getParentNode();
1495 }
1496
1497 return null;
1498 }
1499
1500
1501
1502
1503
1504 public int getOffsetTop() {
1505 if (this instanceof HtmlBody) {
1506 return 0;
1507 }
1508
1509 int top = 0;
1510
1511
1512 final WebWindow webWindow = getPage().getEnclosingWindow();
1513 ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1514 top += style.getTop(true, false, false);
1515
1516
1517 final String position = style.getPositionWithInheritance();
1518 if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
1519 return top;
1520 }
1521
1522 final HtmlElement offsetParent = getOffsetParentInternal(false);
1523
1524
1525 DomNode parentNode = getParentNode();
1526 while (parentNode != null && parentNode != offsetParent) {
1527 if (parentNode instanceof HtmlElement element) {
1528 style = webWindow.getComputedStyle(element, null);
1529 top += style.getTop(false, true, true);
1530 }
1531 parentNode = parentNode.getParentNode();
1532 }
1533
1534 if (offsetParent != null) {
1535 style = webWindow.getComputedStyle(this, null);
1536 final boolean thisElementHasTopMargin = style.getMarginTopValue() != 0;
1537
1538 style = webWindow.getComputedStyle(offsetParent, null);
1539 if (!thisElementHasTopMargin) {
1540 top += style.getMarginTopValue();
1541 }
1542 top += style.getPaddingTopValue();
1543 }
1544
1545 return top;
1546 }
1547
1548
1549
1550
1551
1552 public int getOffsetLeft() {
1553 if (this instanceof HtmlBody) {
1554 return 0;
1555 }
1556
1557 int left = 0;
1558
1559
1560 final WebWindow webWindow = getPage().getEnclosingWindow();
1561 ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1562 left += style.getLeft(true, false, false);
1563
1564
1565 final String position = style.getPositionWithInheritance();
1566 if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
1567 return left;
1568 }
1569
1570 final HtmlElement offsetParent = getOffsetParentInternal(false);
1571
1572 DomNode parentNode = getParentNode();
1573 while (parentNode != null && parentNode != offsetParent) {
1574 if (parentNode instanceof HtmlElement element) {
1575 style = webWindow.getComputedStyle(element, null);
1576 left += style.getLeft(true, true, true);
1577 }
1578 parentNode = parentNode.getParentNode();
1579 }
1580
1581 if (offsetParent != null) {
1582 style = webWindow.getComputedStyle(offsetParent, null);
1583 left += style.getMarginLeftValue();
1584 left += style.getPaddingLeftValue();
1585 }
1586
1587 return left;
1588 }
1589
1590
1591
1592
1593
1594 public int getPosX() {
1595 int cumulativeOffset = 0;
1596 final WebWindow webWindow = getPage().getEnclosingWindow();
1597
1598 HtmlElement element = this;
1599 while (element != null) {
1600 cumulativeOffset += element.getOffsetLeft();
1601 if (element != this) {
1602 final ComputedCssStyleDeclaration style =
1603 webWindow.getComputedStyle(element, null);
1604 cumulativeOffset += style.getBorderLeftValue();
1605 }
1606 element = element.getOffsetParentInternal(false);
1607 }
1608
1609 return cumulativeOffset;
1610 }
1611
1612
1613
1614
1615
1616 public int getPosY() {
1617 int cumulativeOffset = 0;
1618 final WebWindow webWindow = getPage().getEnclosingWindow();
1619
1620 HtmlElement element = this;
1621 while (element != null) {
1622 cumulativeOffset += element.getOffsetTop();
1623 if (element != this) {
1624 final ComputedCssStyleDeclaration style =
1625 webWindow.getComputedStyle(element, null);
1626 cumulativeOffset += style.getBorderTopValue();
1627 }
1628 element = element.getOffsetParentInternal(false);
1629 }
1630
1631 return cumulativeOffset;
1632 }
1633
1634
1635
1636
1637 @Override
1638 public DomNode cloneNode(final boolean deep) {
1639 final HtmlElement newNode = (HtmlElement) super.cloneNode(deep);
1640 if (!deep) {
1641 synchronized (attributeListeners_) {
1642 newNode.attributeListeners_.clear();
1643 newNode.attributeListeners_.addAll(attributeListeners_);
1644 }
1645 }
1646
1647 return newNode;
1648 }
1649 }