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
20 import java.io.IOException;
21 import java.util.ArrayList;
22 import java.util.List;
23 import java.util.Locale;
24 import java.util.Map;
25
26 import org.apache.commons.lang3.StringUtils;
27 import org.htmlunit.BrowserVersion;
28 import org.htmlunit.ElementNotFoundException;
29 import org.htmlunit.Page;
30 import org.htmlunit.ScriptResult;
31 import org.htmlunit.SgmlPage;
32 import org.htmlunit.WebAssert;
33 import org.htmlunit.WebClient;
34 import org.htmlunit.html.impl.SelectableTextInput;
35 import org.htmlunit.javascript.HtmlUnitScriptable;
36 import org.htmlunit.javascript.host.dom.Document;
37 import org.htmlunit.javascript.host.dom.MutationObserver;
38 import org.htmlunit.javascript.host.event.Event;
39 import org.htmlunit.javascript.host.event.EventTarget;
40 import org.htmlunit.javascript.host.event.KeyboardEvent;
41 import org.htmlunit.javascript.host.html.HTMLDocument;
42 import org.htmlunit.javascript.host.html.HTMLElement;
43 import org.w3c.dom.Attr;
44 import org.w3c.dom.CDATASection;
45 import org.w3c.dom.Comment;
46 import org.w3c.dom.DOMException;
47 import org.w3c.dom.Element;
48 import org.w3c.dom.EntityReference;
49 import org.w3c.dom.Node;
50 import org.w3c.dom.ProcessingInstruction;
51 import org.w3c.dom.Text;
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73 public abstract class HtmlElement extends DomElement {
74
75
76
77
78 public enum DisplayStyle {
79
80 EMPTY(""),
81
82 NONE("none"),
83
84 BLOCK("block"),
85
86 CONTENTS("contents"),
87
88 INLINE("inline"),
89
90 INLINE_BLOCK("inline-block"),
91
92 LIST_ITEM("list-item"),
93
94 TABLE("table"),
95
96 TABLE_CELL("table-cell"),
97
98 TABLE_COLUMN("table-column"),
99
100 TABLE_COLUMN_GROUP("table-column-group"),
101
102 TABLE_ROW("table-row"),
103
104 TABLE_ROW_GROUP("table-row-group"),
105
106 TABLE_HEADER_GROUP("table-header-group"),
107
108 TABLE_FOOTER_GROUP("table-footer-group"),
109
110 TABLE_CAPTION("table-caption"),
111
112 RUBY("ruby"),
113
114 RUBY_BASE("ruby-base"),
115
116 RUBY_TEXT("ruby-text"),
117
118 RUBY_TEXT_CONTAINER("ruby-text-container");
119
120 private final String value_;
121 DisplayStyle(final String value) {
122 value_ = value;
123 }
124
125
126
127
128
129 public String value() {
130 return value_;
131 }
132 }
133
134
135
136
137
138
139
140 public static final Short TAB_INDEX_OUT_OF_BOUNDS = Short.valueOf(Short.MIN_VALUE);
141
142
143 protected static final String ATTRIBUTE_REQUIRED = "required";
144
145 protected static final String ATTRIBUTE_CHECKED = "checked";
146
147 protected static final String ATTRIBUTE_HIDDEN = "hidden";
148
149
150 private final List<HtmlAttributeChangeListener> attributeListeners_ = new ArrayList<>();
151
152
153 private HtmlForm owningForm_;
154
155 private boolean shiftPressed_;
156 private boolean ctrlPressed_;
157 private boolean altPressed_;
158
159
160
161
162
163
164
165
166
167 protected HtmlElement(final String qualifiedName, final SgmlPage page,
168 final Map<String, DomAttr> attributes) {
169 this(Html.XHTML_NAMESPACE, qualifiedName, page, attributes);
170 }
171
172
173
174
175
176
177
178
179
180
181 protected HtmlElement(final String namespaceURI, final String qualifiedName, final SgmlPage page,
182 final Map<String, DomAttr> attributes) {
183 super(namespaceURI, qualifiedName, page, attributes);
184 }
185
186
187
188
189 @Override
190 protected void setAttributeNS(final String namespaceURI, final String qualifiedName,
191 final String attributeValue, final boolean notifyAttributeChangeListeners,
192 final boolean notifyMutationObservers) {
193
194
195 if (null == getHtmlPageOrNull()) {
196 super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
197 notifyMutationObservers);
198 return;
199 }
200
201 final String oldAttributeValue = getAttribute(qualifiedName);
202 final HtmlPage htmlPage = (HtmlPage) getPage();
203 final boolean mappedElement = isAttachedToPage()
204 && htmlPage != null
205 && (DomElement.NAME_ATTRIBUTE.equals(qualifiedName) || DomElement.ID_ATTRIBUTE.equals(qualifiedName));
206 if (mappedElement) {
207
208 htmlPage.removeMappedElement(this, false, false);
209 }
210
211 final HtmlAttributeChangeEvent event;
212 if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
213 event = new HtmlAttributeChangeEvent(this, qualifiedName, attributeValue);
214 }
215 else {
216 event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
217 }
218
219 super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
220 notifyMutationObservers);
221
222 if (notifyAttributeChangeListeners) {
223 notifyAttributeChangeListeners(event, this, oldAttributeValue, notifyMutationObservers);
224 }
225
226 fireAttributeChangeImpl(event, htmlPage, mappedElement, oldAttributeValue);
227 }
228
229
230
231
232
233
234
235
236 protected static void notifyAttributeChangeListeners(final HtmlAttributeChangeEvent event,
237 final HtmlElement element, final String oldAttributeValue, final boolean notifyMutationObservers) {
238 final List<HtmlAttributeChangeListener> listeners = new ArrayList<>(element.attributeListeners_);
239 if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
240 synchronized (listeners) {
241 for (final HtmlAttributeChangeListener listener : listeners) {
242 if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
243 listener.attributeAdded(event);
244 }
245 }
246 }
247 }
248 else {
249 synchronized (listeners) {
250 for (final HtmlAttributeChangeListener listener : listeners) {
251 if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
252 listener.attributeReplaced(event);
253 }
254 }
255 }
256 }
257 final DomNode parentNode = element.getParentNode();
258 if (parentNode instanceof HtmlElement) {
259 notifyAttributeChangeListeners(event, (HtmlElement) parentNode, oldAttributeValue, notifyMutationObservers);
260 }
261 }
262
263 private void fireAttributeChangeImpl(final HtmlAttributeChangeEvent event,
264 final HtmlPage htmlPage, final boolean mappedElement, final String oldAttributeValue) {
265 if (mappedElement) {
266 htmlPage.addMappedElement(this, false);
267 }
268
269 if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
270 fireHtmlAttributeAdded(event);
271 htmlPage.fireHtmlAttributeAdded(event);
272 }
273 else {
274 fireHtmlAttributeReplaced(event);
275 htmlPage.fireHtmlAttributeReplaced(event);
276 }
277 }
278
279
280
281
282
283
284
285
286
287
288 @Override
289 public Attr setAttributeNode(final Attr attribute) {
290 final String qualifiedName = attribute.getName();
291 final String oldAttributeValue = getAttribute(qualifiedName);
292 final HtmlPage htmlPage = (HtmlPage) getPage();
293 final boolean mappedElement = isAttachedToPage()
294 && htmlPage != null
295 && (DomElement.NAME_ATTRIBUTE.equals(qualifiedName) || DomElement.ID_ATTRIBUTE.equals(qualifiedName));
296 if (mappedElement) {
297 htmlPage.removeMappedElement(this, false, false);
298 }
299
300 final HtmlAttributeChangeEvent event;
301 if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
302 event = new HtmlAttributeChangeEvent(this, qualifiedName, attribute.getValue());
303 }
304 else {
305 event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
306 }
307 notifyAttributeChangeListeners(event, this, oldAttributeValue, true);
308
309 final Attr result = super.setAttributeNode(attribute);
310
311 fireAttributeChangeImpl(event, htmlPage, mappedElement, oldAttributeValue);
312
313 return result;
314 }
315
316
317
318
319
320 @Override
321 public void removeAttribute(final String attributeName) {
322 final String value = getAttribute(attributeName);
323 if (ATTRIBUTE_NOT_DEFINED == value) {
324 return;
325 }
326
327 final HtmlPage htmlPage = getHtmlPageOrNull();
328 final boolean mapped = htmlPage != null
329 && (DomElement.NAME_ATTRIBUTE.equals(attributeName) || DomElement.ID_ATTRIBUTE.equals(attributeName));
330 if (mapped) {
331 htmlPage.removeMappedElement(this, false, false);
332 }
333
334 super.removeAttribute(attributeName);
335
336 if (htmlPage != null) {
337 if (mapped) {
338 htmlPage.addMappedElement(this, false);
339 }
340
341 final HtmlAttributeChangeEvent event = new HtmlAttributeChangeEvent(this, attributeName, value);
342 fireHtmlAttributeRemoved(event);
343 htmlPage.fireHtmlAttributeRemoved(event);
344 }
345 }
346
347
348
349
350
351
352
353
354
355
356
357
358 protected void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) {
359 final DomNode parentNode = getParentNode();
360 if (parentNode instanceof HtmlElement) {
361 ((HtmlElement) parentNode).fireHtmlAttributeAdded(event);
362 }
363 }
364
365
366
367
368
369
370
371
372
373
374
375
376 protected void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) {
377 final DomNode parentNode = getParentNode();
378 if (parentNode instanceof HtmlElement) {
379 ((HtmlElement) parentNode).fireHtmlAttributeReplaced(event);
380 }
381 }
382
383
384
385
386
387
388
389
390
391
392
393
394 protected void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) {
395 synchronized (attributeListeners_) {
396 for (final HtmlAttributeChangeListener listener : attributeListeners_) {
397 listener.attributeRemoved(event);
398 }
399 }
400 final DomNode parentNode = getParentNode();
401 if (parentNode instanceof HtmlElement) {
402 ((HtmlElement) parentNode).fireHtmlAttributeRemoved(event);
403 }
404 }
405
406
407
408
409 @Override
410 public String getNodeName() {
411 final String prefix = getPrefix();
412 if (prefix != null) {
413
414 final StringBuilder name = new StringBuilder(prefix.toLowerCase(Locale.ROOT))
415 .append(':')
416 .append(getLocalName().toLowerCase(Locale.ROOT));
417 return name.toString();
418 }
419 return getLocalName().toLowerCase(Locale.ROOT);
420 }
421
422
423
424
425
426
427
428
429
430 public Short getTabIndex() {
431 final String index = getAttributeDirect("tabindex");
432 if (index == null || index.isEmpty()) {
433 return null;
434 }
435 try {
436 final long l = Long.parseLong(index);
437 if (l >= 0 && l <= Short.MAX_VALUE) {
438 return Short.valueOf((short) l);
439 }
440 return TAB_INDEX_OUT_OF_BOUNDS;
441 }
442 catch (final NumberFormatException e) {
443 return null;
444 }
445 }
446
447
448
449
450
451
452
453 public HtmlElement getEnclosingElement(final String tagName) {
454 final String tagNameLC = tagName.toLowerCase(Locale.ROOT);
455
456 for (DomNode currentNode = getParentNode(); currentNode != null; currentNode = currentNode.getParentNode()) {
457 if (currentNode instanceof HtmlElement && currentNode.getNodeName().equals(tagNameLC)) {
458 return (HtmlElement) currentNode;
459 }
460 }
461 return null;
462 }
463
464
465
466
467
468
469 public HtmlForm getEnclosingForm() {
470 final String formId = getAttribute("form");
471 if (ATTRIBUTE_NOT_DEFINED != formId) {
472 final Element formById = getPage().getElementById(formId);
473 if (formById instanceof HtmlForm) {
474 return (HtmlForm) formById;
475 }
476 return null;
477 }
478
479 if (owningForm_ != null) {
480 return owningForm_;
481 }
482 return (HtmlForm) getEnclosingElement("form");
483 }
484
485
486
487
488
489
490 public HtmlForm getEnclosingFormOrDie() {
491 final HtmlForm form = getEnclosingForm();
492 if (form == null) {
493 throw new IllegalStateException("Element is not contained within a form: " + this);
494 }
495 return form;
496 }
497
498
499
500
501
502
503
504 public void type(final String text) throws IOException {
505 for (final char ch : text.toCharArray()) {
506 type(ch);
507 }
508 }
509
510
511
512
513
514
515
516
517
518
519
520 public Page type(final char c) throws IOException {
521 return type(c, true);
522 }
523
524
525
526
527
528
529
530
531
532
533
534
535 private Page type(final char c, final boolean lastType)
536 throws IOException {
537 if (isDisabledElementAndDisabled()) {
538 return getPage();
539 }
540
541
542 getPage().getWebClient().setCurrentWindow(getPage().getEnclosingWindow());
543
544 final HtmlPage page = (HtmlPage) getPage();
545 if (page.getFocusedElement() != this) {
546 focus();
547 }
548 final boolean isShiftNeeded = KeyboardEvent.isShiftNeeded(c, shiftPressed_);
549
550 final Event shiftDown;
551 final ScriptResult shiftDownResult;
552 if (isShiftNeeded) {
553 shiftDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, KeyboardEvent.DOM_VK_SHIFT,
554 true, ctrlPressed_, altPressed_);
555 shiftDownResult = fireEvent(shiftDown);
556 }
557 else {
558 shiftDown = null;
559 shiftDownResult = null;
560 }
561
562 final Event keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, c,
563 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
564 final ScriptResult keyDownResult = fireEvent(keyDown);
565
566 if (!keyDown.isAborted(keyDownResult)) {
567 final Event keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, c,
568 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
569 final ScriptResult keyPressResult = fireEvent(keyPress);
570
571 if ((shiftDown == null || !shiftDown.isAborted(shiftDownResult))
572 && !keyPress.isAborted(keyPressResult)) {
573 doType(c, lastType);
574 }
575 }
576
577 final WebClient webClient = page.getWebClient();
578 if (this instanceof HtmlTextInput
579 || this instanceof HtmlTextArea
580 || this instanceof HtmlTelInput
581 || this instanceof HtmlNumberInput
582 || this instanceof HtmlSearchInput
583 || this instanceof HtmlPasswordInput) {
584 fireEvent(new KeyboardEvent(this, Event.TYPE_INPUT, c,
585 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_));
586 }
587
588 HtmlElement eventSource = this;
589 if (!isAttachedToPage()) {
590 eventSource = page.getBody();
591 }
592
593 if (eventSource != null) {
594 final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, c,
595 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
596 eventSource.fireEvent(keyUp);
597
598 if (isShiftNeeded) {
599 final Event shiftUp = new KeyboardEvent(this, Event.TYPE_KEY_UP,
600 KeyboardEvent.DOM_VK_SHIFT,
601 false, ctrlPressed_, altPressed_);
602 eventSource.fireEvent(shiftUp);
603 }
604 }
605
606 final HtmlForm form = getEnclosingForm();
607 if (form != null && c == '\n' && isSubmittableByEnter()) {
608 for (final DomElement descendant : form.getDomElementDescendants()) {
609 if (descendant instanceof HtmlSubmitInput) {
610 return descendant.click();
611 }
612 }
613
614 form.submit((SubmittableElement) this);
615 webClient.getJavaScriptEngine().processPostponedActions();
616 }
617 return webClient.getCurrentWindow().getEnclosedPage();
618 }
619
620
621
622
623
624
625
626
627
628
629
630
631
632 public Page type(final int keyCode) {
633 return type(keyCode, true, true, true, true);
634 }
635
636
637
638
639
640
641
642
643
644
645
646
647 public Page type(final Keyboard keyboard) throws IOException {
648 Page page = null;
649
650 final List<Object[]> keys = keyboard.getKeys();
651
652 if (keyboard.isStartAtEnd()) {
653 if (this instanceof SelectableTextInput) {
654 final SelectableTextInput textInput = (SelectableTextInput) this;
655 textInput.setSelectionStart(textInput.getText().length());
656 }
657 else {
658 final DomText domText = getDoTypeNode();
659 if (domText != null) {
660 domText.moveSelectionToEnd();
661 }
662 }
663 }
664
665 for (int i = 0; i < keys.size(); i++) {
666 final Object[] entry = keys.get(i);
667 if (entry.length == 1) {
668 type((char) entry[0], i == keys.size() - 1);
669 }
670 else {
671 final int key = (int) entry[0];
672 final boolean pressed = (boolean) entry[1];
673 switch (key) {
674 case KeyboardEvent.DOM_VK_SHIFT:
675 shiftPressed_ = pressed;
676 break;
677
678 case KeyboardEvent.DOM_VK_CONTROL:
679 ctrlPressed_ = pressed;
680 break;
681
682 case KeyboardEvent.DOM_VK_ALT:
683 altPressed_ = pressed;
684 break;
685
686 default:
687 }
688 if (pressed) {
689 boolean keyPress = true;
690 boolean keyUp = true;
691 switch (key) {
692 case KeyboardEvent.DOM_VK_SHIFT:
693 case KeyboardEvent.DOM_VK_CONTROL:
694 case KeyboardEvent.DOM_VK_ALT:
695 keyPress = false;
696 keyUp = false;
697 break;
698
699 default:
700 }
701 page = type(key, true, keyPress, keyUp, i == keys.size() - 1);
702 }
703 else {
704 page = type(key, false, false, true, i == keys.size() - 1);
705 }
706 }
707 }
708
709 return page;
710 }
711
712 private Page type(final int keyCode,
713 final boolean fireKeyDown, final boolean fireKeyPress, final boolean fireKeyUp,
714 final boolean lastType) {
715 if (isDisabledElementAndDisabled()) {
716 return getPage();
717 }
718
719 final HtmlPage page = (HtmlPage) getPage();
720 if (page.getFocusedElement() != this) {
721 focus();
722 }
723
724 final Event keyDown;
725 final ScriptResult keyDownResult;
726 if (fireKeyDown) {
727 keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, keyCode, shiftPressed_, ctrlPressed_, altPressed_);
728 keyDownResult = fireEvent(keyDown);
729 }
730 else {
731 keyDown = null;
732 keyDownResult = null;
733 }
734
735 final BrowserVersion browserVersion = page.getWebClient().getBrowserVersion();
736
737 final Event keyPress;
738 final ScriptResult keyPressResult;
739 if (fireKeyPress && browserVersion.hasFeature(KEYBOARD_EVENT_SPECIAL_KEYPRESS)) {
740 keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, keyCode,
741 shiftPressed_, ctrlPressed_, altPressed_);
742
743 keyPressResult = fireEvent(keyPress);
744 }
745 else {
746 keyPress = null;
747 keyPressResult = null;
748 }
749
750 if (keyDown != null && !keyDown.isAborted(keyDownResult)
751 && (keyPress == null || !keyPress.isAborted(keyPressResult))) {
752 doType(keyCode, lastType);
753 }
754
755 if (this instanceof HtmlTextInput
756 || this instanceof HtmlTextArea
757 || this instanceof HtmlTelInput
758 || this instanceof HtmlNumberInput
759 || this instanceof HtmlSearchInput
760 || this instanceof HtmlPasswordInput) {
761 final Event input = new KeyboardEvent(this, Event.TYPE_INPUT, keyCode,
762 shiftPressed_, ctrlPressed_, altPressed_);
763 fireEvent(input);
764 }
765
766 if (fireKeyUp) {
767 final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, keyCode,
768 shiftPressed_, ctrlPressed_, altPressed_);
769 fireEvent(keyUp);
770 }
771
772
773
774
775
776
777
778
779
780
781
782
783
784 return page.getWebClient().getCurrentWindow().getEnclosedPage();
785 }
786
787
788
789
790
791
792 protected void doType(final char c, final boolean lastType) {
793 final DomText domText = getDoTypeNode();
794 if (domText != null) {
795 domText.doType(c, this, lastType);
796 }
797 }
798
799
800
801
802
803
804
805
806
807 protected void doType(final int keyCode, final boolean lastType) {
808 final DomText domText = getDoTypeNode();
809 if (domText != null) {
810 domText.doType(keyCode, this, lastType);
811 }
812 }
813
814
815
816
817
818 private DomText getDoTypeNode() {
819 final HTMLElement scriptElement = getScriptableObject();
820 if (scriptElement.isIsContentEditable()
821 || "on".equals(((Document) scriptElement.getOwnerDocument()).getDesignMode())) {
822
823 DomNodeList<DomNode> children = getChildNodes();
824 while (!children.isEmpty()) {
825 final DomNode lastChild = children.get(children.size() - 1);
826 if (lastChild instanceof DomText) {
827 return (DomText) lastChild;
828 }
829 children = lastChild.getChildNodes();
830 }
831
832 final DomText domText = new DomText(getPage(), "");
833 appendChild(domText);
834 return domText;
835 }
836 return null;
837 }
838
839
840
841
842
843
844 protected void typeDone(final String newValue, final boolean notifyAttributeChangeListeners) {
845
846 }
847
848
849
850
851
852
853 protected boolean acceptChar(final char c) {
854
855
856 return (c < '\uE000' || c > '\uF8FF')
857 && (c == ' ' || c == '\t' || c == '\u3000' || c == '\u2006' || !Character.isWhitespace(c));
858 }
859
860
861
862
863
864
865 protected boolean isSubmittableByEnter() {
866 return false;
867 }
868
869
870
871
872
873
874
875
876
877
878
879
880 public final <E extends HtmlElement> E getOneHtmlElementByAttribute(final String elementName,
881 final String attributeName,
882 final String attributeValue) throws ElementNotFoundException {
883
884 WebAssert.notNull("elementName", elementName);
885 WebAssert.notNull("attributeName", attributeName);
886 WebAssert.notNull("attributeValue", attributeValue);
887
888 final List<E> list = getElementsByAttribute(elementName, attributeName, attributeValue);
889
890 if (list.isEmpty()) {
891 throw new ElementNotFoundException(elementName, attributeName, attributeValue);
892 }
893
894 return list.get(0);
895 }
896
897
898
899
900
901
902
903
904
905
906 @SuppressWarnings("unchecked")
907 public final <E extends HtmlElement> List<E> getElementsByAttribute(
908 final String elementName,
909 final String attributeName,
910 final String attributeValue) {
911
912 final List<E> list = new ArrayList<>();
913 final String lowerCaseTagName = elementName.toLowerCase(Locale.ROOT);
914
915 for (final HtmlElement next : getHtmlElementDescendants()) {
916 if (next.getTagName().equals(lowerCaseTagName)) {
917 final String attValue = next.getAttribute(attributeName);
918 if (attValue.equals(attributeValue)) {
919 list.add((E) next);
920 }
921 }
922 }
923 return list;
924 }
925
926
927
928
929
930
931
932
933
934 public final HtmlElement appendChildIfNoneExists(final String tagName) {
935 final HtmlElement child;
936 final List<HtmlElement> children = getStaticElementsByTagName(tagName);
937 if (children.isEmpty()) {
938
939 child = (HtmlElement) ((HtmlPage) getPage()).createElement(tagName);
940 appendChild(child);
941 }
942 else {
943
944 child = children.get(0);
945 }
946 return child;
947 }
948
949
950
951
952
953
954
955 public final void removeChild(final String tagName, final int i) {
956 final List<HtmlElement> children = getStaticElementsByTagName(tagName);
957 if (i >= 0 && i < children.size()) {
958 children.get(i).remove();
959 }
960 }
961
962
963
964
965
966
967
968
969 public final boolean hasEventHandlers(final String eventName) {
970 if (getPage().getWebClient().isJavaScriptEngineEnabled()) {
971 final HtmlUnitScriptable jsObj = getScriptableObject();
972 if (jsObj instanceof EventTarget) {
973 return ((EventTarget) jsObj).hasEventHandlers(eventName);
974 }
975 }
976 return false;
977 }
978
979
980
981
982
983
984
985
986
987 public void addHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
988 WebAssert.notNull("listener", listener);
989 synchronized (attributeListeners_) {
990 attributeListeners_.add(listener);
991 }
992 }
993
994
995
996
997
998
999
1000
1001
1002 public void removeHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
1003 WebAssert.notNull("listener", listener);
1004 synchronized (attributeListeners_) {
1005 attributeListeners_.remove(listener);
1006 }
1007 }
1008
1009
1010
1011
1012 @Override
1013 protected void checkChildHierarchy(final Node childNode) throws DOMException {
1014 if (!((childNode instanceof Element) || (childNode instanceof Text)
1015 || (childNode instanceof Comment) || (childNode instanceof ProcessingInstruction)
1016 || (childNode instanceof CDATASection) || (childNode instanceof EntityReference))) {
1017 throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
1018 "The Element may not have a child of this type: " + childNode.getNodeType());
1019 }
1020 super.checkChildHierarchy(childNode);
1021 }
1022
1023
1024
1025
1026
1027
1028
1029 public void setOwningForm(final HtmlForm form) {
1030 owningForm_ = form;
1031 }
1032
1033
1034
1035
1036
1037 @Override
1038 protected boolean isAttributeCaseSensitive() {
1039 return false;
1040 }
1041
1042
1043
1044
1045
1046
1047
1048
1049 public final String getLangAttribute() {
1050 return getAttributeDirect("lang");
1051 }
1052
1053
1054
1055
1056
1057
1058
1059
1060 public final String getXmlLangAttribute() {
1061 return getAttribute("xml:lang");
1062 }
1063
1064
1065
1066
1067
1068
1069
1070
1071 public final String getTextDirectionAttribute() {
1072 return getAttributeDirect("dir");
1073 }
1074
1075
1076
1077
1078
1079
1080
1081
1082 public final String getOnClickAttribute() {
1083 return getAttributeDirect("onclick");
1084 }
1085
1086
1087
1088
1089
1090
1091
1092
1093 public final String getOnDblClickAttribute() {
1094 return getAttributeDirect("ondblclick");
1095 }
1096
1097
1098
1099
1100
1101
1102
1103
1104 public final String getOnMouseDownAttribute() {
1105 return getAttributeDirect("onmousedown");
1106 }
1107
1108
1109
1110
1111
1112
1113
1114
1115 public final String getOnMouseUpAttribute() {
1116 return getAttributeDirect("onmouseup");
1117 }
1118
1119
1120
1121
1122
1123
1124
1125
1126 public final String getOnMouseOverAttribute() {
1127 return getAttributeDirect("onmouseover");
1128 }
1129
1130
1131
1132
1133
1134
1135
1136
1137 public final String getOnMouseMoveAttribute() {
1138 return getAttributeDirect("onmousemove");
1139 }
1140
1141
1142
1143
1144
1145
1146
1147
1148 public final String getOnMouseOutAttribute() {
1149 return getAttributeDirect("onmouseout");
1150 }
1151
1152
1153
1154
1155
1156
1157
1158
1159 public final String getOnKeyPressAttribute() {
1160 return getAttributeDirect("onkeypress");
1161 }
1162
1163
1164
1165
1166
1167
1168
1169
1170 public final String getOnKeyDownAttribute() {
1171 return getAttributeDirect("onkeydown");
1172 }
1173
1174
1175
1176
1177
1178
1179
1180
1181 public final String getOnKeyUpAttribute() {
1182 return getAttributeDirect("onkeyup");
1183 }
1184
1185
1186
1187
1188 @Override
1189 public String getCanonicalXPath() {
1190 final DomNode parent = getParentNode();
1191 if (parent.getNodeType() == DOCUMENT_NODE) {
1192 return "/" + getNodeName();
1193 }
1194 return parent.getCanonicalXPath() + '/' + getXPathToken();
1195 }
1196
1197
1198
1199
1200 private String getXPathToken() {
1201 final DomNode parent = getParentNode();
1202 int total = 0;
1203 int nodeIndex = 0;
1204 for (final DomNode child : parent.getChildren()) {
1205 if (child.getNodeType() == ELEMENT_NODE && child.getNodeName().equals(getNodeName())) {
1206 total++;
1207 }
1208 if (child == this) {
1209 nodeIndex = total;
1210 }
1211 }
1212
1213 if (nodeIndex == 1 && total == 1) {
1214 return getNodeName();
1215 }
1216 return getNodeName() + '[' + nodeIndex + ']';
1217 }
1218
1219
1220
1221
1222 public boolean isHidden() {
1223 return ATTRIBUTE_NOT_DEFINED != getAttributeDirect(ATTRIBUTE_HIDDEN);
1224 }
1225
1226
1227
1228
1229
1230 public void setHidden(final String hidden) {
1231 if ("false".equalsIgnoreCase(hidden)) {
1232 removeAttribute(ATTRIBUTE_HIDDEN);
1233 }
1234
1235 if (StringUtils.isNotEmpty(hidden)) {
1236 setAttribute(ATTRIBUTE_HIDDEN, "");
1237 }
1238 }
1239
1240
1241
1242
1243
1244 public void setHidden(final boolean hidden) {
1245 if (hidden) {
1246 setAttribute("hidden", "");
1247 return;
1248 }
1249
1250 removeAttribute("hidden");
1251 }
1252
1253
1254
1255
1256
1257 @Override
1258 public boolean isDisplayed() {
1259 if (isHidden()) {
1260 return false;
1261 }
1262 return super.isDisplayed();
1263 }
1264
1265
1266
1267
1268
1269
1270
1271
1272 public DisplayStyle getDefaultStyleDisplay() {
1273 return DisplayStyle.BLOCK;
1274 }
1275
1276
1277
1278
1279
1280
1281
1282 protected final String getSrcAttributeNormalized() {
1283
1284
1285
1286 final String attrib = getAttributeDirect(SRC_ATTRIBUTE);
1287 if (ATTRIBUTE_NOT_DEFINED == attrib) {
1288 return attrib;
1289 }
1290
1291 return StringUtils.replaceChars(attrib, "\r\n", "");
1292 }
1293
1294
1295
1296
1297
1298
1299
1300 @Override
1301 protected void detach() {
1302 final SgmlPage page = getPage();
1303 if (!page.getWebClient().isJavaScriptEngineEnabled()) {
1304 super.detach();
1305 return;
1306 }
1307
1308 final HtmlUnitScriptable document = page.getScriptableObject();
1309
1310 if (document instanceof HTMLDocument) {
1311 final HTMLDocument doc = (HTMLDocument) document;
1312 final Object activeElement = doc.getActiveElement();
1313
1314 if (activeElement == getScriptableObject()) {
1315 if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1316 ((HtmlPage) page).setFocusedElement(null);
1317 }
1318 else {
1319 ((HtmlPage) page).setElementWithFocus(null);
1320 }
1321 }
1322 else {
1323 for (final DomNode child : getChildNodes()) {
1324 if (activeElement == child.getScriptableObject()) {
1325 if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1326 ((HtmlPage) page).setFocusedElement(null);
1327 }
1328 else {
1329 ((HtmlPage) page).setElementWithFocus(null);
1330 }
1331
1332 break;
1333 }
1334 }
1335 }
1336 }
1337 super.detach();
1338 }
1339
1340
1341
1342
1343 @Override
1344 public boolean handles(final Event event) {
1345 if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
1346 return this instanceof SubmittableElement || getTabIndex() != null;
1347 }
1348
1349 if (isDisabledElementAndDisabled()) {
1350 return false;
1351 }
1352 return super.handles(event);
1353 }
1354
1355
1356
1357
1358
1359 protected boolean isShiftPressed() {
1360 return shiftPressed_;
1361 }
1362
1363
1364
1365
1366
1367 public boolean isCtrlPressed() {
1368 return ctrlPressed_;
1369 }
1370
1371
1372
1373
1374
1375 public boolean isAltPressed() {
1376 return altPressed_;
1377 }
1378
1379
1380
1381
1382
1383 public boolean isValid() {
1384 return !isRequiredSupported()
1385 || ATTRIBUTE_NOT_DEFINED == getAttributeDirect(ATTRIBUTE_REQUIRED)
1386 || !getAttributeDirect(VALUE_ATTRIBUTE).isEmpty();
1387 }
1388
1389
1390
1391
1392
1393 protected boolean isRequiredSupported() {
1394 return false;
1395 }
1396
1397
1398
1399
1400 public boolean isRequired() {
1401 return isRequiredSupported() && hasAttribute(ATTRIBUTE_REQUIRED);
1402 }
1403
1404
1405
1406
1407 public boolean isOptional() {
1408 return isRequiredSupported() && !hasAttribute(ATTRIBUTE_REQUIRED);
1409 }
1410
1411
1412
1413
1414
1415 public void setRequired(final boolean required) {
1416 if (isRequiredSupported()) {
1417 if (required) {
1418 setAttribute(ATTRIBUTE_REQUIRED, ATTRIBUTE_REQUIRED);
1419 }
1420 else {
1421 removeAttribute(ATTRIBUTE_REQUIRED);
1422 }
1423 }
1424 }
1425
1426
1427
1428
1429 @Override
1430 public DomNode cloneNode(final boolean deep) {
1431 final HtmlElement newNode = (HtmlElement) super.cloneNode(deep);
1432 if (!deep) {
1433 synchronized (attributeListeners_) {
1434 newNode.attributeListeners_.clear();
1435 newNode.attributeListeners_.addAll(attributeListeners_);
1436 }
1437 }
1438
1439 return newNode;
1440 }
1441 }