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