1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.javascript.host;
16
17 import static org.htmlunit.BrowserVersionFeatures.EVENT_SCROLL_UIEVENT;
18 import static org.htmlunit.BrowserVersionFeatures.JS_OUTER_HTML_THROWS_FOR_DETACHED;
19 import static org.htmlunit.html.DomElement.ATTRIBUTE_NOT_DEFINED;
20 import static org.htmlunit.javascript.configuration.SupportedBrowser.CHROME;
21 import static org.htmlunit.javascript.configuration.SupportedBrowser.EDGE;
22 import static org.htmlunit.javascript.configuration.SupportedBrowser.FF;
23 import static org.htmlunit.javascript.configuration.SupportedBrowser.FF_ESR;
24
25 import java.io.IOException;
26 import java.io.Serializable;
27 import java.util.ArrayList;
28 import java.util.HashMap;
29 import java.util.Map;
30 import java.util.Objects;
31 import java.util.function.Predicate;
32 import java.util.regex.Pattern;
33
34 import org.apache.commons.logging.LogFactory;
35 import org.htmlunit.SgmlPage;
36 import org.htmlunit.corejs.javascript.BaseFunction;
37 import org.htmlunit.corejs.javascript.Context;
38 import org.htmlunit.corejs.javascript.Function;
39 import org.htmlunit.corejs.javascript.NativeObject;
40 import org.htmlunit.corejs.javascript.Scriptable;
41 import org.htmlunit.corejs.javascript.ScriptableObject;
42 import org.htmlunit.corejs.javascript.TopLevel;
43 import org.htmlunit.corejs.javascript.VarScope;
44 import org.htmlunit.corejs.javascript.WithScope;
45 import org.htmlunit.css.ComputedCssStyleDeclaration;
46 import org.htmlunit.css.ElementCssStyleDeclaration;
47 import org.htmlunit.cssparser.parser.CSSException;
48 import org.htmlunit.html.DomAttr;
49 import org.htmlunit.html.DomCDataSection;
50 import org.htmlunit.html.DomCharacterData;
51 import org.htmlunit.html.DomComment;
52 import org.htmlunit.html.DomElement;
53 import org.htmlunit.html.DomNode;
54 import org.htmlunit.html.DomText;
55 import org.htmlunit.html.HtmlElement;
56 import org.htmlunit.html.HtmlElement.DisplayStyle;
57 import org.htmlunit.html.HtmlTemplate;
58 import org.htmlunit.javascript.HtmlUnitScriptable;
59 import org.htmlunit.javascript.JavaScriptEngine;
60 import org.htmlunit.javascript.configuration.JsxClass;
61 import org.htmlunit.javascript.configuration.JsxConstructor;
62 import org.htmlunit.javascript.configuration.JsxFunction;
63 import org.htmlunit.javascript.configuration.JsxGetter;
64 import org.htmlunit.javascript.configuration.JsxSetter;
65 import org.htmlunit.javascript.host.css.CSSStyleDeclaration;
66 import org.htmlunit.javascript.host.dom.Attr;
67 import org.htmlunit.javascript.host.dom.DOMException;
68 import org.htmlunit.javascript.host.dom.DOMTokenList;
69 import org.htmlunit.javascript.host.dom.Node;
70 import org.htmlunit.javascript.host.dom.NodeList;
71 import org.htmlunit.javascript.host.event.Event;
72 import org.htmlunit.javascript.host.event.EventHandler;
73 import org.htmlunit.javascript.host.event.UIEvent;
74 import org.htmlunit.javascript.host.html.HTMLCollection;
75 import org.htmlunit.javascript.host.html.HTMLElement;
76 import org.htmlunit.javascript.host.html.HTMLElement.ProxyDomNode;
77 import org.htmlunit.javascript.host.html.HTMLScriptElement;
78 import org.htmlunit.javascript.host.html.HTMLStyleElement;
79 import org.htmlunit.javascript.host.html.HTMLTemplateElement;
80 import org.htmlunit.util.StringUtils;
81 import org.xml.sax.SAXException;
82
83
84
85
86
87
88
89
90
91
92
93 @JsxClass(domClass = DomElement.class)
94 public class Element extends Node {
95
96 static final String POSITION_BEFORE_BEGIN = "beforebegin";
97 static final String POSITION_AFTER_BEGIN = "afterbegin";
98 static final String POSITION_BEFORE_END = "beforeend";
99 static final String POSITION_AFTER_END = "afterend";
100
101 private static final Pattern CLASS_NAMES_SPLIT_PATTERN = Pattern.compile("\\s");
102 private static final Pattern PRINT_NODE_PATTERN = Pattern.compile(" {2}");
103 private static final Pattern PRINT_NODE_QUOTE_PATTERN = Pattern.compile("\"");
104
105 private NamedNodeMap attributes_;
106 private Map<String, HTMLCollection> elementsByTagName_;
107 private int scrollLeft_;
108 private int scrollTop_;
109 private CSSStyleDeclaration style_;
110
111
112
113
114 @Override
115 @JsxConstructor
116 public void jsConstructor() {
117 super.jsConstructor();
118 }
119
120
121
122
123
124 @Override
125 public void setDomNode(final DomNode domNode) {
126 super.setDomNode(domNode);
127
128 final Window window = getWindow();
129 setParentScope(new WithScope(getTopLevelScope(getParentScope()), window.getDocument()));
130
131 style_ = new CSSStyleDeclaration(this, new ElementCssStyleDeclaration(getDomNodeOrDie()));
132
133
134
135 final DomElement htmlElt = (DomElement) domNode;
136 for (final DomAttr attr : htmlElt.getAttributesMap().values()) {
137 final String eventName = StringUtils.toRootLowerCase(attr.getName());
138 if (eventName.startsWith("on")) {
139 createEventHandler(eventName.substring(2), attr.getValue());
140 }
141 }
142 }
143
144
145
146
147
148
149 protected void createEventHandler(final String eventName, final String attrValue) {
150 final DomElement htmlElt = getDomNodeOrDie();
151
152
153 final BaseFunction eventHandler = new EventHandler(htmlElt, eventName, attrValue);
154 eventHandler.setPrototype(ScriptableObject.getClassPrototype(getParentScope(), "Function"));
155
156 setEventHandler(eventName, eventHandler);
157 }
158
159
160
161
162
163 @JsxGetter
164 public String getTagName() {
165 return getNodeName();
166 }
167
168
169
170
171
172
173 @Override
174 @JsxGetter
175 public NamedNodeMap getAttributes() {
176 if (attributes_ == null) {
177 attributes_ = createAttributesObject();
178 }
179 return attributes_;
180 }
181
182
183
184
185
186 protected NamedNodeMap createAttributesObject() {
187 return new NamedNodeMap(getDomNodeOrDie());
188 }
189
190
191
192
193
194 @JsxFunction
195 public String getAttribute(final String attributeName) {
196 String value = getDomNodeOrDie().getAttribute(attributeName);
197
198 if (ATTRIBUTE_NOT_DEFINED == value) {
199 value = null;
200 }
201
202 return value;
203 }
204
205
206
207
208
209
210
211 @JsxFunction
212 public void setAttribute(final String name, final String value) {
213 getDomNodeOrDie().setAttribute(name, value);
214 }
215
216
217
218
219
220
221 @JsxFunction
222 public HTMLCollection getElementsByTagName(final String tagName) {
223 if (elementsByTagName_ == null) {
224 elementsByTagName_ = new HashMap<>();
225 }
226
227 final String searchTagName;
228 final boolean caseSensitive;
229 final DomNode dom = getDomNodeOrNull();
230 if (dom == null) {
231 searchTagName = StringUtils.toRootLowerCase(tagName);
232 caseSensitive = false;
233 }
234 else {
235 final SgmlPage page = dom.getPage();
236 if (page != null && page.hasCaseSensitiveTagNames()) {
237 searchTagName = tagName;
238 caseSensitive = true;
239 }
240 else {
241 searchTagName = StringUtils.toRootLowerCase(tagName);
242 caseSensitive = false;
243 }
244 }
245
246 HTMLCollection collection = elementsByTagName_.get(searchTagName);
247 if (collection != null) {
248 return collection;
249 }
250
251 final DomNode node = getDomNodeOrDie();
252 collection = new HTMLCollection(node, false);
253 if (StringUtils.equalsChar('*', tagName)) {
254 collection.setIsMatchingPredicate((Predicate<DomNode> & Serializable) nodeToMatch -> true);
255 }
256 else {
257 collection.setIsMatchingPredicate(
258 (Predicate<DomNode> & Serializable) nodeToMatch -> {
259 if (caseSensitive) {
260 return searchTagName.equals(nodeToMatch.getNodeName());
261 }
262 return searchTagName.equalsIgnoreCase(nodeToMatch.getNodeName());
263 });
264 }
265
266 elementsByTagName_.put(tagName, collection);
267
268 return collection;
269 }
270
271
272
273
274
275
276 @JsxFunction
277 public HtmlUnitScriptable getAttributeNode(final String name) {
278 final Map<String, DomAttr> attributes = getDomNodeOrDie().getAttributesMap();
279 for (final DomAttr attr : attributes.values()) {
280 if (attr.getName().equals(name)) {
281 return attr.getScriptableObject();
282 }
283 }
284 return null;
285 }
286
287
288
289
290
291
292
293
294 @JsxFunction
295 public HTMLCollection getElementsByTagNameNS(final Object namespaceURI, final String localName) {
296 final HTMLCollection elements = new HTMLCollection(getDomNodeOrDie(), false);
297 elements.setIsMatchingPredicate(
298 (Predicate<DomNode> & Serializable)
299 node -> ("*".equals(namespaceURI) || Objects.equals(namespaceURI, node.getNamespaceURI()))
300 && ("*".equals(localName) || Objects.equals(localName, node.getLocalName())));
301 return elements;
302 }
303
304
305
306
307
308
309
310
311 @JsxFunction
312 public boolean hasAttribute(final String name) {
313 return getDomNodeOrDie().hasAttribute(name);
314 }
315
316
317
318
319 @Override
320 @JsxFunction
321 public boolean hasAttributes() {
322 return super.hasAttributes();
323 }
324
325
326
327
328 @Override
329 public DomElement getDomNodeOrDie() {
330 return (DomElement) super.getDomNodeOrDie();
331 }
332
333
334
335
336
337 @JsxFunction
338 public void removeAttribute(final String name) {
339 getDomNodeOrDie().removeAttribute(name);
340 }
341
342
343
344
345
346
347 @JsxFunction
348 public DOMRect getBoundingClientRect() {
349 final DOMRect textRectangle = new DOMRect(1, 1, 0, 0);
350 textRectangle.setParentScope(getTopLevelScope(getParentScope()));
351 textRectangle.setPrototype(getPrototype(textRectangle.getClass()));
352 return textRectangle;
353 }
354
355
356
357
358 @Override
359 @JsxGetter
360 public int getChildElementCount() {
361 return getDomNodeOrDie().getChildElementCount();
362 }
363
364
365
366
367 @Override
368 @JsxGetter
369 public Element getFirstElementChild() {
370 return super.getFirstElementChild();
371 }
372
373
374
375
376 @Override
377 @JsxGetter
378 public Element getLastElementChild() {
379 return super.getLastElementChild();
380 }
381
382
383
384
385
386 @JsxGetter
387 public Element getNextElementSibling() {
388 final DomElement child = getDomNodeOrDie().getNextElementSibling();
389 if (child != null) {
390 return child.getScriptableObject();
391 }
392 return null;
393 }
394
395
396
397
398
399 @JsxGetter
400 public Element getPreviousElementSibling() {
401 final DomElement child = getDomNodeOrDie().getPreviousElementSibling();
402 if (child != null) {
403 return child.getScriptableObject();
404 }
405 return null;
406 }
407
408
409
410
411
412
413
414 @Override
415 public Element getParentElement() {
416 Node parent = getParent();
417 while (parent != null && !(parent instanceof Element)) {
418 parent = parent.getParent();
419 }
420 return (Element) parent;
421 }
422
423
424
425
426 @Override
427 @JsxGetter
428 public HTMLCollection getChildren() {
429 return super.getChildren();
430 }
431
432
433
434
435
436 @JsxGetter
437 public DOMTokenList getClassList() {
438 return new DOMTokenList(this, "class");
439 }
440
441
442
443
444
445
446
447 @JsxFunction
448 public String getAttributeNS(final String namespaceURI, final String localName) {
449 final String value = getDomNodeOrDie().getAttributeNS(namespaceURI, localName);
450 if (ATTRIBUTE_NOT_DEFINED == value) {
451 return null;
452 }
453 return value;
454 }
455
456
457
458
459
460
461
462
463
464
465 @JsxFunction
466 public boolean hasAttributeNS(final String namespaceURI, final String localName) {
467 return getDomNodeOrDie().hasAttributeNS(namespaceURI, localName);
468 }
469
470
471
472
473
474
475
476 @JsxFunction
477 public void setAttributeNS(final String namespaceURI, final String qualifiedName, final String value) {
478 getDomNodeOrDie().setAttributeNS(namespaceURI, qualifiedName, value);
479 }
480
481
482
483
484
485
486 @JsxFunction
487 public void removeAttributeNS(final String namespaceURI, final String localName) {
488 getDomNodeOrDie().removeAttributeNS(namespaceURI, localName);
489 }
490
491
492
493
494
495
496 @JsxFunction
497 public Attr setAttributeNode(final Attr newAtt) {
498 final String name = newAtt.getName();
499
500 final NamedNodeMap nodes = getAttributes();
501 final Attr replacedAtt = (Attr) nodes.getNamedItemWithoutSytheticClassAttr(name);
502 if (replacedAtt != null) {
503 replacedAtt.detachFromParent();
504 }
505
506 final DomAttr newDomAttr = newAtt.getDomNodeOrDie();
507 getDomNodeOrDie().setAttributeNode(newDomAttr);
508 return replacedAtt;
509 }
510
511
512
513
514
515
516
517
518 @JsxFunction
519 public NodeList querySelectorAll(final String selectors) {
520 try {
521 return NodeList.staticNodeList(getParentScope(), getDomNodeOrDie().querySelectorAll(selectors));
522 }
523 catch (final CSSException e) {
524 throw JavaScriptEngine.asJavaScriptException(
525 getWindow(),
526 "An invalid or illegal selector was specified (selector: '"
527 + selectors + "' error: " + e.getMessage() + ").",
528 DOMException.SYNTAX_ERR);
529 }
530 }
531
532
533
534
535
536
537 @JsxFunction
538 public Node querySelector(final String selectors) {
539 try {
540 final DomNode node = getDomNodeOrDie().querySelector(selectors);
541 if (node != null) {
542 return node.getScriptableObject();
543 }
544 return null;
545 }
546 catch (final CSSException e) {
547 throw JavaScriptEngine.asJavaScriptException(
548 getWindow(),
549 "An invalid or illegal selector was specified (selector: '"
550 + selectors + "' error: " + e.getMessage() + ").",
551 DOMException.SYNTAX_ERR);
552 }
553 }
554
555
556
557
558
559 @JsxGetter(propertyName = "className")
560 public String getClassName_js() {
561 return getDomNodeOrDie().getAttributeDirect("class");
562 }
563
564
565
566
567
568 @JsxSetter(propertyName = "className")
569 public void setClassName_js(final String className) {
570 getDomNodeOrDie().setAttribute("class", className);
571 }
572
573
574
575
576
577 @JsxGetter
578 public int getClientHeight() {
579 final ComputedCssStyleDeclaration style = getWindow().getWebWindow().getComputedStyle(getDomNodeOrDie(), null);
580 return style.getCalculatedHeight(false, true);
581 }
582
583
584
585
586
587 @JsxGetter
588 public int getClientWidth() {
589 final ComputedCssStyleDeclaration style = getWindow().getWebWindow().getComputedStyle(getDomNodeOrDie(), null);
590 return style.getCalculatedWidth(false, true);
591 }
592
593
594
595
596
597 @JsxGetter
598 public int getClientLeft() {
599 final ComputedCssStyleDeclaration style = getWindow().getWebWindow().getComputedStyle(getDomNodeOrDie(), null);
600 return style.getBorderLeftValue();
601 }
602
603
604
605
606
607 @JsxGetter
608 public int getClientTop() {
609 final ComputedCssStyleDeclaration style = getWindow().getWebWindow().getComputedStyle(getDomNodeOrDie(), null);
610 return style.getBorderTopValue();
611 }
612
613
614
615
616
617
618
619 @JsxFunction
620 public HtmlUnitScriptable getAttributeNodeNS(final String namespaceURI, final String localName) {
621 return getDomNodeOrDie().getAttributeNodeNS(namespaceURI, localName).getScriptableObject();
622 }
623
624
625
626
627
628
629 @JsxFunction
630 public HTMLCollection getElementsByClassName(final String className) {
631 final DomElement elt = getDomNodeOrDie();
632 final String[] classNames = CLASS_NAMES_SPLIT_PATTERN.split(className, 0);
633
634 final HTMLCollection elements = new HTMLCollection(elt, true);
635
636 elements.setIsMatchingPredicate(
637 (Predicate<DomNode> & Serializable)
638 node -> {
639 if (!(node instanceof HtmlElement)) {
640 return false;
641 }
642 String classAttribute = ((HtmlElement) node).getAttributeDirect("class");
643 if (ATTRIBUTE_NOT_DEFINED == classAttribute) {
644 return false;
645 }
646
647 classAttribute = " " + classAttribute + " ";
648 for (final String aClassName : classNames) {
649 if (!classAttribute.contains(" " + aClassName + " ")) {
650 return false;
651 }
652 }
653 return true;
654 });
655
656 return elements;
657 }
658
659
660
661
662
663
664 @JsxFunction
665 public DOMRectList getClientRects() {
666 final TopLevel topScope = getTopLevelScope(getParentScope());
667 final DOMRectList rectList = new DOMRectList();
668 rectList.setParentScope(topScope);
669 rectList.setPrototype(getPrototype(rectList.getClass()));
670
671 if (!isDisplayNone() && getDomNodeOrDie().isAttachedToPage()) {
672 final DOMRect rect = new DOMRect(0, 0, 1, 1);
673 rect.setParentScope(topScope);
674 rect.setPrototype(getPrototype(rect.getClass()));
675 rectList.add(rect);
676 }
677
678 return rectList;
679 }
680
681
682
683
684
685 @JsxFunction
686 public Scriptable getAttributeNames() {
687 final org.w3c.dom.NamedNodeMap attributes = getDomNodeOrDie().getAttributes();
688
689 if (attributes.getLength() == 0) {
690 return JavaScriptEngine.newArray(getParentScope(), 0);
691 }
692
693 final ArrayList<String> res = new ArrayList<>();
694 for (int i = 0; i < attributes.getLength(); i++) {
695 res.add(attributes.item(i).getNodeName());
696 }
697
698 return JavaScriptEngine.newArray(getParentScope(), res.toArray());
699 }
700
701
702
703
704
705 protected final boolean isDisplayNone() {
706 Element element = this;
707 while (element != null) {
708 final CSSStyleDeclaration style = element.getWindow().getComputedStyle(element, null);
709 final String display = style.getDisplay();
710 if (DisplayStyle.NONE.value().equals(display)) {
711 return true;
712 }
713 element = element.getParentElement();
714 }
715 return false;
716 }
717
718
719
720
721
722
723
724
725
726
727 @JsxFunction
728 public Node insertAdjacentElement(final String where, final Object insertedElement) {
729 if (insertedElement instanceof Node insertedElementNode) {
730 final DomNode childNode = insertedElementNode.getDomNodeOrDie();
731 final Object[] values = getInsertAdjacentLocation(where);
732 final DomNode node = (DomNode) values[0];
733 final boolean append = ((Boolean) values[1]).booleanValue();
734
735 if (append) {
736 node.appendChild(childNode);
737 }
738 else {
739 node.insertBefore(childNode);
740 }
741 return insertedElementNode;
742 }
743 throw JavaScriptEngine.reportRuntimeError("Passed object is not an element: " + insertedElement);
744 }
745
746
747
748
749
750
751
752
753
754 @JsxFunction
755 public void insertAdjacentText(final String where, final String text) {
756 final Object[] values = getInsertAdjacentLocation(where);
757 final DomNode node = (DomNode) values[0];
758 final boolean append = ((Boolean) values[1]).booleanValue();
759
760 final DomText domText = new DomText(node.getPage(), text);
761
762 if (append) {
763 node.appendChild(domText);
764 }
765 else {
766 node.insertBefore(domText);
767 }
768 }
769
770
771
772
773
774
775
776
777
778
779 private Object[] getInsertAdjacentLocation(final String where) {
780 final DomNode currentNode = getDomNodeOrDie();
781 final DomNode node;
782 final boolean append;
783
784
785 if (POSITION_AFTER_BEGIN.equalsIgnoreCase(where)) {
786 if (currentNode.getFirstChild() == null) {
787
788 node = currentNode;
789 append = true;
790 }
791 else {
792
793 node = currentNode.getFirstChild();
794 append = false;
795 }
796 }
797 else if (POSITION_BEFORE_BEGIN.equalsIgnoreCase(where)) {
798
799 node = currentNode;
800 append = false;
801 }
802 else if (POSITION_BEFORE_END.equalsIgnoreCase(where)) {
803
804 node = currentNode;
805 append = true;
806 }
807 else if (POSITION_AFTER_END.equalsIgnoreCase(where)) {
808 if (currentNode.getNextSibling() == null) {
809
810 node = currentNode.getParentNode();
811 append = true;
812 }
813 else {
814
815 node = currentNode.getNextSibling();
816 append = false;
817 }
818 }
819 else {
820 throw JavaScriptEngine.reportRuntimeError("Illegal position value: \"" + where + "\"");
821 }
822
823 if (append) {
824 return new Object[] {node, Boolean.TRUE};
825 }
826 return new Object[] {node, Boolean.FALSE};
827 }
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842 @JsxFunction
843 public void insertAdjacentHTML(final String position, final String text) {
844 final Object[] values = getInsertAdjacentLocation(position);
845 final DomNode domNode = (DomNode) values[0];
846 final boolean append = ((Boolean) values[1]).booleanValue();
847
848
849 final DomNode proxyDomNode = new ProxyDomNode(domNode.getPage(), domNode, append);
850 parseHtmlSnippet(proxyDomNode, text);
851 }
852
853
854
855
856
857
858
859
860
861
862 @JsxFunction({CHROME, EDGE, FF})
863 public static void moveBefore(final Context context, final VarScope scope,
864 final Scriptable thisObj, final Object[] args, final Function function) {
865 Node.moveBefore(context, scope, thisObj, args, function);
866 }
867
868
869
870
871
872
873 private static void parseHtmlSnippet(final DomNode target, final String source) {
874 try {
875 target.parseHtmlSnippet(source);
876 }
877 catch (final IOException | SAXException e) {
878 LogFactory.getLog(HtmlElement.class).error("Unexpected exception occurred while parsing HTML snippet", e);
879 throw JavaScriptEngine.reportRuntimeError("Unexpected exception occurred while parsing HTML snippet: "
880 + e.getMessage());
881 }
882 }
883
884
885
886
887
888 @JsxFunction
889 public String getHTML() {
890
891 return getInnerHTML();
892 }
893
894
895
896
897
898 @JsxGetter
899 public String getInnerHTML() {
900 try {
901 DomNode domNode = getDomNodeOrDie();
902 if (this instanceof HTMLTemplateElement) {
903 domNode = ((HtmlTemplate) getDomNodeOrDie()).getContent();
904 }
905 return getInnerHTML(domNode);
906 }
907 catch (final IllegalStateException e) {
908 throw JavaScriptEngine.typeError(e.getMessage());
909 }
910 }
911
912
913
914
915
916 @JsxSetter
917 public void setInnerHTML(final Object value) {
918 final DomElement domNode;
919 try {
920 domNode = getDomNodeOrDie();
921 }
922 catch (final IllegalStateException e) {
923 throw JavaScriptEngine.typeError(e.getMessage());
924 }
925
926 String html = null;
927 if (value != null) {
928 html = JavaScriptEngine.toString(value);
929 if (StringUtils.isEmptyString(html)) {
930 html = null;
931 }
932 }
933
934 try {
935 domNode.setInnerHtml(html);
936 }
937 catch (final IOException | SAXException e) {
938 LogFactory.getLog(HtmlElement.class).error("Unexpected exception occurred while parsing HTML snippet", e);
939 throw JavaScriptEngine.reportRuntimeError("Unexpected exception occurred while parsing HTML snippet: "
940 + e.getMessage());
941 }
942 }
943
944
945
946
947
948
949 protected String getInnerHTML(final DomNode domNode) {
950 final StringBuilder buf = new StringBuilder();
951
952 final String tagName = getTagName();
953 boolean isPlain = "SCRIPT".equals(tagName);
954
955 isPlain = isPlain || "STYLE".equals(tagName);
956
957
958 printChildren(buf, domNode, !isPlain);
959 return buf.toString();
960 }
961
962
963
964
965
966
967 @JsxGetter
968 public String getOuterHTML() {
969 final StringBuilder buf = new StringBuilder();
970
971 printNode(buf, getDomNodeOrDie(), true);
972 return buf.toString();
973 }
974
975
976
977
978
979 @JsxSetter
980 public void setOuterHTML(final Object value) {
981 final DomNode domNode = getDomNodeOrDie();
982 final DomNode parent = domNode.getParentNode();
983 if (null == parent) {
984 if (getBrowserVersion().hasFeature(JS_OUTER_HTML_THROWS_FOR_DETACHED)) {
985 throw JavaScriptEngine.asJavaScriptException(
986 getWindow(),
987 "outerHTML is readonly for detached nodes",
988 DOMException.NO_MODIFICATION_ALLOWED_ERR);
989 }
990 return;
991 }
992
993 if (value == null) {
994 domNode.remove();
995 return;
996 }
997
998 final String valueStr = JavaScriptEngine.toString(value);
999 if (valueStr.isEmpty()) {
1000 domNode.remove();
1001 return;
1002 }
1003
1004 final DomNode nextSibling = domNode.getNextSibling();
1005 domNode.remove();
1006
1007 final DomNode target;
1008 final boolean append;
1009 if (nextSibling != null) {
1010 target = nextSibling;
1011 append = false;
1012 }
1013 else {
1014 target = parent;
1015 append = true;
1016 }
1017
1018 final DomNode proxyDomNode = new ProxyDomNode(target.getPage(), target, append);
1019 parseHtmlSnippet(proxyDomNode, valueStr);
1020 }
1021
1022
1023
1024
1025
1026
1027
1028 protected final void printChildren(final StringBuilder builder, final DomNode node, final boolean html) {
1029 if (node instanceof HtmlTemplate template) {
1030
1031 for (final DomNode child : template.getContent().getChildren()) {
1032 printNode(builder, child, html);
1033 }
1034 return;
1035 }
1036
1037 for (final DomNode child : node.getChildren()) {
1038 printNode(builder, child, html);
1039 }
1040 }
1041
1042 protected void printNode(final StringBuilder builder, final DomNode node, final boolean html) {
1043 if (node instanceof DomComment) {
1044 if (html) {
1045
1046 final String s = PRINT_NODE_PATTERN.matcher(node.getNodeValue()).replaceAll(" ");
1047 builder.append("<!--").append(s).append("-->");
1048 }
1049 }
1050 else if (node instanceof DomCDataSection) {
1051 builder.append("<![CDATA[").append(node.getNodeValue()).append("]]>");
1052 }
1053 else if (node instanceof DomCharacterData) {
1054
1055 String s = node.getNodeValue();
1056 if (html) {
1057 s = StringUtils.escapeXmlChars(s);
1058 }
1059 builder.append(s);
1060 }
1061 else if (html) {
1062 final DomElement element = (DomElement) node;
1063 final Element scriptObject = node.getScriptableObject();
1064 final String tag = element.getTagName();
1065
1066 Element htmlElement = null;
1067 if (scriptObject instanceof HTMLElement) {
1068 htmlElement = scriptObject;
1069 }
1070 builder.append('<').append(tag);
1071 for (final DomAttr attr : element.getAttributesMap().values()) {
1072 if (!attr.getSpecified()) {
1073 continue;
1074 }
1075
1076 final String name = attr.getName();
1077 final String value = PRINT_NODE_QUOTE_PATTERN.matcher(attr.getValue()).replaceAll(""");
1078 builder.append(' ').append(name).append("=\"").append(value).append('\"');
1079 }
1080 builder.append('>');
1081
1082 final boolean isHtml = !(scriptObject instanceof HTMLScriptElement)
1083 && !(scriptObject instanceof HTMLStyleElement);
1084 printChildren(builder, node, isHtml);
1085 if (null == htmlElement || !htmlElement.isEndTagForbidden()) {
1086 builder.append("</").append(tag).append('>');
1087 }
1088 }
1089 else {
1090 if (node instanceof HtmlElement element) {
1091 if (StringUtils.equalsChar('p', element.getTagName())) {
1092 int i = builder.length() - 1;
1093 while (i >= 0 && Character.isWhitespace(builder.charAt(i))) {
1094 i--;
1095 }
1096 builder.setLength(i + 1);
1097 builder.append('\n');
1098 }
1099 if (!"script".equals(element.getTagName())) {
1100 printChildren(builder, node, html);
1101 }
1102 }
1103 }
1104 }
1105
1106
1107
1108
1109
1110
1111 protected boolean isEndTagForbidden() {
1112 return false;
1113 }
1114
1115
1116
1117
1118
1119 @JsxGetter
1120 public String getId() {
1121 return getDomNodeOrDie().getId();
1122 }
1123
1124
1125
1126
1127
1128 @JsxSetter
1129 public void setId(final String newId) {
1130 getDomNodeOrDie().setId(newId);
1131 }
1132
1133
1134
1135
1136
1137 @JsxFunction
1138 public void removeAttributeNode(final Attr attribute) {
1139 final String name = attribute.getName();
1140 final String namespaceUri = attribute.getNamespaceURI();
1141 removeAttributeNS(namespaceUri, name);
1142 }
1143
1144
1145
1146
1147
1148
1149 @JsxGetter
1150 public int getScrollTop() {
1151
1152
1153 if (scrollTop_ < 0) {
1154 scrollTop_ = 0;
1155 }
1156 else if (scrollTop_ > 0) {
1157 final ComputedCssStyleDeclaration style =
1158 getWindow().getWebWindow().getComputedStyle(getDomNodeOrDie(), null);
1159 if (!style.isScrollable(false)) {
1160 scrollTop_ = 0;
1161 }
1162 }
1163 return scrollTop_;
1164 }
1165
1166
1167
1168
1169
1170 @JsxSetter
1171 public void setScrollTop(final int scroll) {
1172 scrollTop_ = scroll;
1173 }
1174
1175
1176
1177
1178
1179
1180 @JsxGetter
1181 public int getScrollLeft() {
1182
1183
1184 if (scrollLeft_ < 0) {
1185 scrollLeft_ = 0;
1186 }
1187 else if (scrollLeft_ > 0) {
1188 final ComputedCssStyleDeclaration style =
1189 getWindow().getWebWindow().getComputedStyle(getDomNodeOrDie(), null);
1190 if (!style.isScrollable(true)) {
1191 scrollLeft_ = 0;
1192 }
1193 }
1194 return scrollLeft_;
1195 }
1196
1197
1198
1199
1200
1201 @JsxSetter
1202 public void setScrollLeft(final int scroll) {
1203 scrollLeft_ = scroll;
1204 }
1205
1206
1207
1208
1209
1210
1211 @JsxGetter
1212 public int getScrollHeight() {
1213 return getClientHeight();
1214 }
1215
1216
1217
1218
1219
1220
1221 @JsxGetter
1222 public int getScrollWidth() {
1223 return getClientWidth();
1224 }
1225
1226
1227
1228
1229
1230 protected CSSStyleDeclaration getStyle() {
1231 return style_;
1232 }
1233
1234
1235
1236
1237
1238 protected void setStyle(final String style) {
1239 getStyle().setCssText(style);
1240 }
1241
1242
1243
1244
1245
1246
1247 @JsxFunction
1248 public void scroll(final Scriptable x, final Scriptable y) {
1249 scrollTo(x, y);
1250 }
1251
1252
1253
1254
1255
1256
1257 @JsxFunction
1258 public void scrollBy(final Scriptable x, final Scriptable y) {
1259 int xOff = 0;
1260 int yOff = 0;
1261 if (y != null) {
1262 xOff = JavaScriptEngine.toInt32(x);
1263 yOff = JavaScriptEngine.toInt32(y);
1264 }
1265 else {
1266 if (!(x instanceof NativeObject)) {
1267 throw JavaScriptEngine.typeError("eee");
1268 }
1269 if (x.has("left", x)) {
1270 xOff = JavaScriptEngine.toInt32(x.get("left", x));
1271 }
1272 if (x.has("top", x)) {
1273 yOff = JavaScriptEngine.toInt32(x.get("top", x));
1274 }
1275 }
1276
1277 setScrollLeft(getScrollLeft() + xOff);
1278 setScrollTop(getScrollTop() + yOff);
1279
1280 fireScrollEvent(this);
1281 }
1282
1283 private void fireScrollEvent(final Node node) {
1284 final Event event;
1285 if (getBrowserVersion().hasFeature(EVENT_SCROLL_UIEVENT)) {
1286 event = new UIEvent(node, Event.TYPE_SCROLL);
1287 }
1288 else {
1289 event = new Event(node, Event.TYPE_SCROLL);
1290 event.setCancelable(false);
1291 }
1292 event.setBubbles(false);
1293 node.fireEvent(event);
1294 }
1295
1296 private void fireScrollEvent(final Window window) {
1297 final Event event;
1298 if (getBrowserVersion().hasFeature(EVENT_SCROLL_UIEVENT)) {
1299 event = new UIEvent(window.getDocument(), Event.TYPE_SCROLL);
1300 }
1301 else {
1302 event = new Event(window.getDocument(), Event.TYPE_SCROLL);
1303 event.setCancelable(false);
1304 }
1305 window.fireEvent(event);
1306 }
1307
1308
1309
1310
1311
1312
1313 @JsxFunction
1314 public void scrollTo(final Scriptable x, final Scriptable y) {
1315 int xOff;
1316 int yOff;
1317 if (y != null) {
1318 xOff = JavaScriptEngine.toInt32(x);
1319 yOff = JavaScriptEngine.toInt32(y);
1320 }
1321 else {
1322 if (!(x instanceof NativeObject)) {
1323 throw JavaScriptEngine.typeError("eee");
1324 }
1325
1326 xOff = getScrollLeft();
1327 yOff = getScrollTop();
1328 if (x.has("left", x)) {
1329 xOff = JavaScriptEngine.toInt32(x.get("left", x));
1330 }
1331 if (x.has("top", x)) {
1332 yOff = JavaScriptEngine.toInt32(x.get("top", x));
1333 }
1334 }
1335
1336 setScrollLeft(xOff);
1337 setScrollTop(yOff);
1338
1339 fireScrollEvent(this);
1340 }
1341
1342
1343
1344
1345
1346
1347 @JsxFunction
1348 public void scrollIntoView() {
1349
1350
1351
1352
1353 Node parent = getParent();
1354 while (parent != null) {
1355 if (parent instanceof HTMLElement) {
1356 fireScrollEvent(parent);
1357 }
1358
1359 parent = parent.getParent();
1360 }
1361 fireScrollEvent(getWindow());
1362 }
1363
1364
1365
1366
1367
1368 @JsxFunction({CHROME, EDGE})
1369 public void scrollIntoViewIfNeeded() {
1370
1371 }
1372
1373
1374
1375
1376 @Override
1377 @JsxGetter
1378 public String getPrefix() {
1379 return super.getPrefix();
1380 }
1381
1382
1383
1384
1385 @Override
1386 @JsxGetter
1387 public String getLocalName() {
1388 return super.getLocalName();
1389 }
1390
1391
1392
1393
1394 @Override
1395 @JsxGetter
1396 public String getNamespaceURI() {
1397 return super.getNamespaceURI();
1398 }
1399
1400
1401
1402
1403
1404 @JsxGetter({CHROME, EDGE})
1405 public Function getOnbeforecopy() {
1406 return getEventHandler(Event.TYPE_BEFORECOPY);
1407 }
1408
1409
1410
1411
1412
1413 @JsxSetter({CHROME, EDGE})
1414 public void setOnbeforecopy(final Object onbeforecopy) {
1415 setEventHandler(Event.TYPE_BEFORECOPY, onbeforecopy);
1416 }
1417
1418
1419
1420
1421
1422 @JsxGetter({CHROME, EDGE})
1423 public Function getOnbeforecut() {
1424 return getEventHandler(Event.TYPE_BEFORECUT);
1425 }
1426
1427
1428
1429
1430
1431 @JsxSetter({CHROME, EDGE})
1432 public void setOnbeforecut(final Object onbeforecut) {
1433 setEventHandler(Event.TYPE_BEFORECUT, onbeforecut);
1434 }
1435
1436
1437
1438
1439
1440 @JsxGetter({CHROME, EDGE})
1441 public Function getOnbeforepaste() {
1442 return getEventHandler(Event.TYPE_BEFOREPASTE);
1443 }
1444
1445
1446
1447
1448
1449 @JsxSetter({CHROME, EDGE})
1450 public void setOnbeforepaste(final Object onbeforepaste) {
1451 setEventHandler(Event.TYPE_BEFOREPASTE, onbeforepaste);
1452 }
1453
1454
1455
1456
1457
1458 @JsxGetter({CHROME, EDGE})
1459 public Function getOnsearch() {
1460 return getEventHandler(Event.TYPE_SEARCH);
1461 }
1462
1463
1464
1465
1466
1467 @JsxSetter({CHROME, EDGE})
1468 public void setOnsearch(final Object onsearch) {
1469 setEventHandler(Event.TYPE_SEARCH, onsearch);
1470 }
1471
1472
1473
1474
1475
1476 @JsxGetter({CHROME, EDGE})
1477 public Function getOnwebkitfullscreenchange() {
1478 return getEventHandler(Event.TYPE_WEBKITFULLSCREENCHANGE);
1479 }
1480
1481
1482
1483
1484
1485 @JsxSetter({CHROME, EDGE})
1486 public void setOnwebkitfullscreenchange(final Object onwebkitfullscreenchange) {
1487 setEventHandler(Event.TYPE_WEBKITFULLSCREENCHANGE, onwebkitfullscreenchange);
1488 }
1489
1490
1491
1492
1493
1494 @JsxGetter({CHROME, EDGE})
1495 public Function getOnwebkitfullscreenerror() {
1496 return getEventHandler(Event.TYPE_WEBKITFULLSCREENERROR);
1497 }
1498
1499
1500
1501
1502
1503 @JsxSetter({CHROME, EDGE})
1504 public void setOnwebkitfullscreenerror(final Object onwebkitfullscreenerror) {
1505 setEventHandler(Event.TYPE_WEBKITFULLSCREENERROR, onwebkitfullscreenerror);
1506 }
1507
1508
1509
1510
1511
1512 public Function getOnwheel() {
1513 return getEventHandler(Event.TYPE_WHEEL);
1514 }
1515
1516
1517
1518
1519
1520 public void setOnwheel(final Object onwheel) {
1521 setEventHandler(Event.TYPE_WHEEL, onwheel);
1522 }
1523
1524
1525
1526
1527 @Override
1528 @JsxFunction
1529 public void remove() {
1530 super.remove();
1531 }
1532
1533
1534
1535
1536
1537
1538 @JsxFunction({FF, FF_ESR})
1539 public void setCapture(final boolean retargetToElement) {
1540
1541 }
1542
1543
1544
1545
1546 @JsxFunction({FF, FF_ESR})
1547 public void releaseCapture() {
1548
1549 }
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560 @JsxFunction
1561 public static void before(final Context context, final VarScope scope,
1562 final Scriptable thisObj, final Object[] args, final Function function) {
1563 Node.before(context, thisObj, args, function);
1564 }
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575 @JsxFunction
1576 public static void after(final Context context, final VarScope scope,
1577 final Scriptable thisObj, final Object[] args, final Function function) {
1578 Node.after(context, thisObj, args, function);
1579 }
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589 @JsxFunction
1590 public static void replaceWith(final Context context, final VarScope scope,
1591 final Scriptable thisObj, final Object[] args, final Function function) {
1592 Node.replaceWith(context, thisObj, args, function);
1593 }
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604 @JsxFunction
1605 public static boolean matches(final Context context, final VarScope scope,
1606 final Scriptable thisObj, final Object[] args, final Function function) {
1607 if (!(thisObj instanceof Element)) {
1608 throw JavaScriptEngine.typeError("Illegal invocation");
1609 }
1610
1611 final String selectorString = (String) args[0];
1612 try {
1613 final DomNode domNode = ((Element) thisObj).getDomNodeOrNull();
1614 return domNode != null && ((DomElement) domNode).matches(selectorString);
1615 }
1616 catch (final CSSException e) {
1617 throw JavaScriptEngine.asJavaScriptException(
1618 (HtmlUnitScriptable) getTopLevelScope(scope).getGlobalThis(),
1619 "An invalid or illegal selector was specified (selector: '"
1620 + selectorString + "' error: " + e.getMessage() + ").",
1621 DOMException.SYNTAX_ERR);
1622 }
1623 }
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634 @JsxFunction({FF, FF_ESR})
1635 public static boolean mozMatchesSelector(final Context context, final VarScope scope,
1636 final Scriptable thisObj, final Object[] args, final Function function) {
1637 return matches(context, scope, thisObj, args, function);
1638 }
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649 @JsxFunction
1650 public static boolean webkitMatchesSelector(final Context context, final VarScope scope,
1651 final Scriptable thisObj, final Object[] args, final Function function) {
1652 return matches(context, scope, thisObj, args, function);
1653 }
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665 @JsxFunction
1666 public static Element closest(final Context context, final VarScope scope,
1667 final Scriptable thisObj, final Object[] args, final Function function) {
1668 if (!(thisObj instanceof Element)) {
1669 throw JavaScriptEngine.typeError("Illegal invocation");
1670 }
1671
1672 final String selectorString = (String) args[0];
1673 try {
1674 final DomNode domNode = ((Element) thisObj).getDomNodeOrNull();
1675 if (domNode == null) {
1676 return null;
1677 }
1678 final DomElement elem = domNode.closest(selectorString);
1679 if (elem == null) {
1680 return null;
1681 }
1682 return elem.getScriptableObject();
1683 }
1684 catch (final CSSException e) {
1685 throw JavaScriptEngine.syntaxError(
1686 "An invalid or illegal selector was specified (selector: '"
1687 + selectorString + "' error: " + e.getMessage() + ").");
1688 }
1689 }
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
1705
1706 @JsxFunction
1707 public boolean toggleAttribute(final String name, final Object force) {
1708 if (JavaScriptEngine.isUndefined(force)) {
1709 if (hasAttribute(name)) {
1710 removeAttribute(name);
1711 return false;
1712 }
1713 setAttribute(name, "");
1714 return true;
1715 }
1716 if (JavaScriptEngine.toBoolean(force)) {
1717 setAttribute(name, "");
1718 return true;
1719 }
1720 removeAttribute(name);
1721 return false;
1722 }
1723
1724
1725
1726
1727
1728
1729
1730
1731
1732
1733
1734 @JsxFunction
1735 public static void append(final Context context, final VarScope scope,
1736 final Scriptable thisObj, final Object[] args, final Function function) {
1737 if (!(thisObj instanceof Element)) {
1738 throw JavaScriptEngine.typeError("Illegal invocation");
1739 }
1740
1741 Node.append(context, thisObj, args, function);
1742 }
1743
1744
1745
1746
1747
1748
1749
1750
1751
1752
1753
1754 @JsxFunction
1755 public static void prepend(final Context context, final VarScope scope,
1756 final Scriptable thisObj, final Object[] args, final Function function) {
1757 if (!(thisObj instanceof Element)) {
1758 throw JavaScriptEngine.typeError("Illegal invocation");
1759 }
1760
1761 Node.prepend(context, thisObj, args, function);
1762 }
1763
1764
1765
1766
1767
1768
1769
1770
1771
1772
1773
1774 @JsxFunction
1775 public static void replaceChildren(final Context context, final VarScope scope,
1776 final Scriptable thisObj, final Object[] args, final Function function) {
1777 if (!(thisObj instanceof Element)) {
1778 throw JavaScriptEngine.typeError("Illegal invocation");
1779 }
1780
1781 Node.replaceChildren(context, thisObj, args, function);
1782 }
1783 }