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