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