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