View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
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   * JavaScript host object for {@code Element}.
86   *
87   * @author Ahmed Ashour
88   * @author Marc Guillemot
89   * @author Sudhan Moghe
90   * @author Ronald Brill
91   * @author Frank Danek
92   * @author Anton Demydenko
93   *
94   * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element">MDN Documentation</a>
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_; // for performance and for equality (==)
112     private int scrollLeft_;
113     private int scrollTop_;
114     private CSSStyleDeclaration style_;
115 
116     /**
117      * Creates an instance of this object.
118      */
119     @Override
120     @JsxConstructor
121     public void jsConstructor() {
122         super.jsConstructor();
123     }
124 
125     /**
126      * Sets the DOM node that corresponds to this JavaScript object.
127      *
128      * @param domNode the DOM node
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         // CSSStyleDeclaration uses the parent scope
137         style_ = new CSSStyleDeclaration(this, new ElementCssStyleDeclaration(getDomNodeOrDie()));
138 
139         // Convert JavaScript snippets defined in the attribute map to executable event handlers.
140         // Should be called only on construction.
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      * Creates the event handler function from the attribute value.
152      *
153      * @param eventName the event name (e.g. {@code onclick})
154      * @param attrValue the attribute value
155      */
156     protected void createEventHandler(final String eventName, final String attrValue) {
157         final DomElement htmlElt = getDomNodeOrDie();
158 
159         // TODO: check that it is an "allowed" event for the browser, and take care to the case
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      * Returns the tag name of this element.
168      *
169      * @return the tag name
170      */
171     @JsxGetter
172     public String getTagName() {
173         return getNodeName();
174     }
175 
176     /**
177      * Returns the attributes of this XML element.
178      *
179      * @return the attributes of this XML element
180      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/attributes">MDN Documentation</a>
181      */
182     @Override
183     @JsxGetter
184     public NamedNodeMap getAttributes() {
185         if (attributes_ == null) {
186             attributes_ = createAttributesObject();
187         }
188         return attributes_;
189     }
190 
191     /**
192      * Creates the JS object for the {@code attributes} property. This object will be cached.
193      *
194      * @return the JS object
195      */
196     protected NamedNodeMap createAttributesObject() {
197         return new NamedNodeMap(getDomNodeOrDie());
198     }
199 
200     /**
201      * Returns the value of the specified attribute, or {@code null} if the attribute is not defined.
202      *
203      * @param attributeName the name of the attribute to retrieve
204      * @return the value of the specified attribute, or {@code null} if not defined
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      * Sets the specified attribute to the given value.
219      *
220      * @param name the name of the attribute to set
221      * @param value the value to set the attribute to
222      */
223     @JsxFunction
224     public void setAttribute(final String name, final String value) {
225         getDomNodeOrDie().setAttribute(name, value);
226     }
227 
228     /**
229      * Returns all descendant elements with the specified tag name.
230      *
231      * @param tagName the tag name to search for
232      * @return all descendant elements with the specified tag name
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      * Retrieves an attribute node by name.
286      *
287      * @param name the name of the attribute to retrieve
288      * @return the {@link Attr} node with the specified name, or {@code null} if there is no such attribute
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      * Returns a live {@link HTMLCollection} of elements with the given tag name belonging to the given namespace.
303      *
304      * @param namespaceURI the namespace URI of elements to look for
305      * @param localName the local name of elements to look for, or {@code "*"} to match all elements
306      * @return a live {@link HTMLCollection} of found elements in document order
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      * Returns {@code true} when an attribute with the given name is specified on this element or has a default value.
320      *
321      * @param name the name of the attribute to look for
322      * @return {@code true} if the attribute exists or has a default value
323      */
324     @JsxFunction
325     public boolean hasAttribute(final String name) {
326         return getDomNodeOrDie().hasAttribute(name);
327     }
328 
329     /**
330      * {@inheritDoc}
331      */
332     @Override
333     @JsxFunction
334     public boolean hasAttributes() {
335         return super.hasAttributes();
336     }
337 
338     /**
339      * {@inheritDoc}
340      */
341     @Override
342     public DomElement getDomNodeOrDie() {
343         return (DomElement) super.getDomNodeOrDie();
344     }
345 
346     /**
347      * Removes the attribute with the specified name.
348      *
349      * @param name the name of the attribute to remove
350      */
351     @JsxFunction
352     public void removeAttribute(final String name) {
353         getDomNodeOrDie().removeAttribute(name);
354     }
355 
356     /**
357      * Returns the bounding rectangle of this element relative to the viewport.
358      *
359      * @return a {@link DOMRect} object describing the element's size and position
360      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/getBoundingClientRect">MDN Documentation</a>
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      * {@inheritDoc}
372      */
373     @Override
374     @JsxGetter
375     public int getChildElementCount() {
376         return getDomNodeOrDie().getChildElementCount();
377     }
378 
379     /**
380      * {@inheritDoc}
381      */
382     @Override
383     @JsxGetter
384     public Element getFirstElementChild() {
385         return super.getFirstElementChild();
386     }
387 
388     /**
389      * {@inheritDoc}
390      */
391     @Override
392     @JsxGetter
393     public Element getLastElementChild() {
394         return super.getLastElementChild();
395     }
396 
397     /**
398      * Returns the next sibling that is an element.
399      *
400      * @return the next element sibling, or {@code null} if none
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      * Returns the previous sibling that is an element.
413      *
414      * @return the previous element sibling, or {@code null} if none
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      * Returns the first ancestor that is an {@link Element}. Skips non-{@link Element} nodes.
427      *
428      * @return the parent element
429      * @see #getParent()
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      * {@inheritDoc}
442      */
443     @Override
444     @JsxGetter
445     public HTMLCollection getChildren() {
446         return super.getChildren();
447     }
448 
449     /**
450      * Returns the token list of the {@code class} attribute.
451      *
452      * @return the token list of the {@code class} attribute
453      */
454     @JsxGetter
455     public DOMTokenList getClassList() {
456         return new DOMTokenList(this, "class");
457     }
458 
459     /**
460      * Returns the value of the specified attribute in the given namespace,
461      * or {@code null} if the attribute is not defined.
462      *
463      * @param namespaceURI the namespace URI
464      * @param localName the local name of the attribute to retrieve
465      * @return the attribute value, or {@code null} if not found
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      * Returns {@code true} if the element has an attribute with the given namespace URI and local name.
478      *
479      * @param namespaceURI the namespace URI
480      * @param localName the local name of the attribute to look for
481      * @return {@code true} if the attribute exists
482      */
483     @JsxFunction
484     public boolean hasAttributeNS(final String namespaceURI, final String localName) {
485         return getDomNodeOrDie().hasAttributeNS(namespaceURI, localName);
486     }
487 
488     /**
489      * Sets the attribute with the given namespace URI and qualified name to the given value.
490      *
491      * @param namespaceURI the namespace URI
492      * @param qualifiedName the qualified name of the attribute
493      * @param value the new attribute value
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      * Removes the attribute with the given namespace URI and local name.
502      *
503      * @param namespaceURI the namespace URI of the attribute to remove
504      * @param localName the local name of the attribute to remove
505      */
506     @JsxFunction
507     public void removeAttributeNS(final String namespaceURI, final String localName) {
508         getDomNodeOrDie().removeAttributeNS(namespaceURI, localName);
509     }
510 
511     /**
512      * Sets the attribute node for the specified attribute, replacing the existing node if present.
513      *
514      * @param newAtt the attribute node to set
515      * @return the replaced attribute node, if any
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      * Returns a static {@link NodeList} of all descendant elements matching the given CSS selector(s).
534      *
535      * @param selectors the CSS selector(s)
536      * @return the static node list of matching elements
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      * Returns the first descendant element that matches the specified CSS selector,
554      * or {@code null} if no matches are found.
555      *
556      * @param selectors the CSS selector(s)
557      * @return the first matching element, or {@code null}
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      * Returns the value of the {@code class} attribute.
579      *
580      * @return the class name
581      */
582     @JsxGetter(propertyName = "className")
583     public String getClassName_js() {
584         return getDomNodeOrDie().getAttributeDirect("class");
585     }
586 
587     /**
588      * Sets the {@code class} attribute for this element.
589      *
590      * @param className the new class name
591      */
592     @JsxSetter(propertyName = "className")
593     public void setClassName_js(final String className) {
594         getDomNodeOrDie().setAttribute("class", className);
595     }
596 
597     /**
598      * Returns the {@code clientHeight} property.
599      *
600      * @return the {@code clientHeight} property
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      * Returns the {@code clientWidth} property.
610      *
611      * @return the {@code clientWidth} property
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      * Returns the {@code clientLeft} property.
621      *
622      * @return the {@code clientLeft} property
623      */
624     @JsxGetter
625     public int getClientLeft() {
626         final ComputedCssStyleDeclaration style = getWindow().getWebWindow().getComputedStyle(getDomNodeOrDie(), null);
627         return style.getBorderLeftValue();
628     }
629 
630     /**
631      * Returns the {@code clientTop} property.
632      *
633      * @return the {@code clientTop} property
634      */
635     @JsxGetter
636     public int getClientTop() {
637         final ComputedCssStyleDeclaration style = getWindow().getWebWindow().getComputedStyle(getDomNodeOrDie(), null);
638         return style.getBorderTopValue();
639     }
640 
641     /**
642      * Returns the attribute node with the given namespace URI and local name.
643      *
644      * @param namespaceURI the namespace URI
645      * @param localName the local name of the attribute to retrieve
646      * @return the specified attribute node, or {@code null} if not found
647      */
648     @JsxFunction
649     public HtmlUnitScriptable getAttributeNodeNS(final String namespaceURI, final String localName) {
650         return getDomNodeOrDie().getAttributeNodeNS(namespaceURI, localName).getScriptableObject();
651     }
652 
653     /**
654      * Returns all descendant elements that have the specified class name(s).
655      *
656      * @param className the class name(s) to search for (space-separated)
657      * @return all descendant elements with the specified class name(s)
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; // probably better performance as most elements won't have a class attribute
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      * Returns a collection of {@link DOMRect} objects that describe the layout of the element's
691      * content on screen. Each rectangle represents one line of the element's content.
692      *
693      * @return a {@link DOMRectList} describing the element's line boxes
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      * Returns the attribute names of this element as an array of strings.
714      * Returns an empty array if the element has no attributes.
715      *
716      * @return the attribute names as an array
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      * Returns whether the {@code display} style of this element or any ancestor is {@code none}.
736      *
737      * @return {@code true} if the element or any ancestor has {@code display: none}
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      * Inserts the given element at the specified position relative to this element.
754      *
755      * @param where specifies where to insert the element; one of {@code beforebegin},
756      *        {@code afterbegin}, {@code beforeend}, or {@code afterend} (case-insensitive)
757      * @param insertedElement the element to insert
758      * @return the inserted element
759      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentElement">MDN Documentation</a>
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      * Inserts the given text at the specified position relative to this element.
782      *
783      * @param where specifies where to insert the text; one of {@code beforebegin},
784      *        {@code afterbegin}, {@code beforeend}, or {@code afterend} (case-insensitive)
785      * @param text the text to insert
786      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentText">MDN Documentation</a>
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         // add the new nodes
796         if (append) {
797             node.appendChild(domText);
798         }
799         else {
800             node.insertBefore(domText);
801         }
802     }
803 
804     /**
805      * Returns the target node and insertion mode for the given adjacent position string.
806      * Used by {@link #insertAdjacentHTML(String, String)},
807      * {@link #insertAdjacentElement(String, Object)}, and
808      * {@link #insertAdjacentText(String, String)}.
809      *
810      * @param where specifies where to insert; one of {@code beforebegin},
811      *        {@code afterbegin}, {@code beforeend}, or {@code afterend} (case-insensitive)
812      * @return an array of [{@link DomNode} parentNode, {@link Boolean} append]
813      */
814     private Object[] getInsertAdjacentLocation(final String where) {
815         final DomNode currentNode = getDomNodeOrDie();
816         final DomNode node;
817         final boolean append;
818 
819         // compute the where and how the new nodes should be added
820         if (POSITION_AFTER_BEGIN.equalsIgnoreCase(where)) {
821             if (currentNode.getFirstChild() == null) {
822                 // new nodes should append to the children of current node
823                 node = currentNode;
824                 append = true;
825             }
826             else {
827                 // new nodes should be inserted before first child
828                 node = currentNode.getFirstChild();
829                 append = false;
830             }
831         }
832         else if (POSITION_BEFORE_BEGIN.equalsIgnoreCase(where)) {
833             // new nodes should be inserted before current node
834             node = currentNode;
835             append = false;
836         }
837         else if (POSITION_BEFORE_END.equalsIgnoreCase(where)) {
838             // new nodes should append to the children of current node
839             node = currentNode;
840             append = true;
841         }
842         else if (POSITION_AFTER_END.equalsIgnoreCase(where)) {
843             if (currentNode.getNextSibling() == null) {
844                 // new nodes should append to the children of parent node
845                 node = currentNode.getParentNode();
846                 append = true;
847             }
848             else {
849                 // new nodes should be inserted before current node's next sibling
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      * Parses the given text as HTML or XML and inserts the resulting nodes at the specified position.
866      *
867      * @param position specifies where to insert the nodes; one of {@code beforebegin},
868      *        {@code afterbegin}, {@code beforeend}, or {@code afterend} (case-insensitive)
869      * @param text the HTML or XML text to parse and insert
870      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/insertAdjacentHTML">MDN Documentation</a>
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         // add the new nodes
879         final DomNode proxyDomNode = new ProxyDomNode(domNode.getPage(), domNode, append);
880         parseHtmlSnippet(proxyDomNode, text);
881     }
882 
883     /**
884      * Moves a given node inside this element as a direct child, before a given reference node.
885      *
886      * @param context the JavaScript context
887      * @param scope the scope
888      * @param thisObj the scriptable
889      * @param args the arguments passed into the method
890      * @param function the function
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      * Parses the specified HTML source code and appends the resulting content at the specified target location.
900      *
901      * @param target the node indicating the position at which the parsed content should be placed
902      * @param source the HTML code to parse
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      * Returns the contents of this node as HTML, ignoring shadow DOM parameters as shadow DOM is not supported.
917      *
918      * @return the contents of this node as HTML
919      */
920     @JsxFunction
921     public String getHTML() {
922         // ignore the params because we have no shadow dom support so far
923         return getInnerHTML();
924     }
925 
926     /**
927      * Returns the {@code innerHTML} of this element.
928      *
929      * @return the contents of this node as HTML
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      * Replaces all child elements of this element with the supplied HTML value.
947      *
948      * @param value the new HTML content for this element
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      * Helper for {@code getInnerHTML}, reusable by {@code HTMLTemplateElement}.
980      *
981      * @param domNode the node to serializep
982      * @return the contents of this node as HTML
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         // we can't rely on DomNode.asXml because it adds indentation and new lines
998         printChildren(buf, domNode, !isPlain, isPlain || rawText ? TextEscape.NONE : mode);
999         return buf.toString();
1000     }
1001 
1002     /**
1003      * HTML serialization writes the text children of these elements as is.
1004      */
1005     private static boolean isRawTextElement(final DomElement element) {
1006         if (!(element instanceof HtmlElement) || element.getPage() instanceof XHtmlPage) {
1007             return false; // svg/math elements and XML serialization always escape
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      * Returns the {@code outerHTML} of this element, including the element's own tags.
1018      *
1019      * @return the contents of this node as HTML, including the opening and closing tags
1020      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/outerHTML">MDN Documentation</a>
1021      */
1022     @JsxGetter
1023     public String getOuterHTML() {
1024         final StringBuilder buf = new StringBuilder();
1025         // we can't rely on DomNode.asXml because it adds indentation and new lines
1026         printNode(buf, getDomNodeOrDie(), true, TextEscape.HTML);
1027         return buf.toString();
1028     }
1029 
1030     /**
1031      * Replaces this element (including all child elements) with the supplied HTML value.
1032      *
1033      * @param value the new HTML to replace this element
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      * Serializes the children of the given node to the provided builder.
1087      *
1088      * @param builder the builder to write to
1089      * @param node the node whose children are to be serialized
1090      * @param html whether to use HTML serialization
1091      * @param escape whether to use HTML escaping
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                 // Remove whitespace sequences.
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);   // & < > only, nbsp stays as is
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("&quot;");
1133                 builder.append(' ').append(name).append("=\"").append(value).append('\"');
1134             }
1135             builder.append('>');
1136 
1137             if (htmlElement != null && htmlElement.isEndTagForbidden()) {
1138                 // void element: neither children nor an end tag are serialized
1139                 return;
1140             }
1141 
1142             // Add the children.
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             // in XML documents script and style are ordinary elements for the serializer
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      * Returns whether the end tag is forbidden for this element.
1174      *
1175      * @return whether the end tag is forbidden
1176      * @see <a href="http://www.w3.org/TR/html4/index/elements.html">HTML 4 specs</a>
1177      */
1178     protected boolean isEndTagForbidden() {
1179         return false;
1180     }
1181 
1182     /**
1183      * Returns the element ID.
1184      *
1185      * @return the ID of this element
1186      */
1187     @JsxGetter
1188     public String getId() {
1189         return getDomNodeOrDie().getId();
1190     }
1191 
1192     /**
1193      * Sets the ID of this element.
1194      *
1195      * @param newId the new ID value for this element
1196      */
1197     @JsxSetter
1198     public void setId(final String newId) {
1199         getDomNodeOrDie().setId(newId);
1200     }
1201 
1202     /**
1203      * Removes the specified attribute node from this element.
1204      *
1205      * @param attribute the attribute node to remove
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      * Returns the {@code scrollTop} value for this element.
1216      *
1217      * @return the {@code scrollTop} value
1218      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollTop">MDN Documentation</a>
1219      */
1220     @JsxGetter
1221     public int getScrollTop() {
1222         // It's easier to perform these checks and adjustments in the getter, rather than in the setter,
1223         // because modifying the CSS style of the element is supposed to affect the attribute value.
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      * Sets the {@code scrollTop} value for this element.
1239      *
1240      * @param scroll the new {@code scrollTop} value
1241      */
1242     @JsxSetter
1243     public void setScrollTop(final int scroll) {
1244         scrollTop_ = scroll;
1245     }
1246 
1247     /**
1248      * Returns the {@code scrollLeft} value for this element.
1249      *
1250      * @return the {@code scrollLeft} value
1251      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollLeft">MDN Documentation</a>
1252      */
1253     @JsxGetter
1254     public int getScrollLeft() {
1255         // It's easier to perform these checks and adjustments in the getter, rather than in the setter,
1256         // because modifying the CSS style of the element is supposed to affect the attribute value.
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      * Sets the {@code scrollLeft} value for this element.
1272      *
1273      * @param scroll the new {@code scrollLeft} value
1274      */
1275     @JsxSetter
1276     public void setScrollLeft(final int scroll) {
1277         scrollLeft_ = scroll;
1278     }
1279 
1280     /**
1281      * Returns the {@code scrollHeight} for this element.
1282      * Currently returns the same value as {@link #getClientHeight()}.
1283      *
1284      * @return the scroll height
1285      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollHeight">MDN Documentation</a>
1286      */
1287     @JsxGetter
1288     public int getScrollHeight() {
1289         return getClientHeight();
1290     }
1291 
1292     /**
1293      * Returns the {@code scrollWidth} for this element.
1294      * Currently returns the same value as {@link #getClientWidth()}.
1295      *
1296      * @return the scroll width
1297      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollWidth">MDN Documentation</a>
1298      */
1299     @JsxGetter
1300     public int getScrollWidth() {
1301         return getClientWidth();
1302     }
1303 
1304     /**
1305      * Returns the style object for this element.
1306      *
1307      * @return the style object for this element
1308      */
1309     protected CSSStyleDeclaration getStyle() {
1310         return style_;
1311     }
1312 
1313     /**
1314      * Sets the CSS text for this element's inline style.
1315      *
1316      * @param style the new CSS style text
1317      */
1318     protected void setStyle(final String style) {
1319         getStyle().setCssText(style);
1320     }
1321 
1322     /**
1323      * Scrolls to a particular set of coordinates inside this element.
1324      *
1325      * @param x the horizontal pixel value to scroll to
1326      * @param y the vertical pixel value to scroll to
1327      */
1328     @JsxFunction
1329     public void scroll(final Scriptable x, final Scriptable y) {
1330         scrollTo(x, y);
1331     }
1332 
1333     /**
1334      * Scrolls the element by the given amount.
1335      *
1336      * @param x the horizontal pixel value to scroll by
1337      * @param y the vertical pixel value to scroll by
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      * Scrolls to a particular set of coordinates inside this element.
1392      *
1393      * @param x the horizontal pixel value to scroll to
1394      * @param y the vertical pixel value to scroll to
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      * Scrolls the element into the visible area of the browser window.
1427      * This implementation triggers the scroll event but does not actually scroll
1428      * (headless environment).
1429      */
1430     @JsxFunction
1431     public void scrollIntoView() {
1432         // do nothing at the moment, only trigger the scroll event
1433 
1434         // we do not really handle scrollable elements (we are headless)
1435         // we trigger the event for the whole parent tree (to inform all)
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      * Scrolls the element into the visible area if needed.
1449      * This is a no-op implementation.
1450      */
1451     @JsxFunction({CHROME, EDGE})
1452     public void scrollIntoViewIfNeeded() {
1453         /* do nothing at the moment */
1454     }
1455 
1456     /**
1457      * {@inheritDoc}
1458      */
1459     @Override
1460     @JsxGetter
1461     public String getPrefix() {
1462         return super.getPrefix();
1463     }
1464 
1465     /**
1466      * {@inheritDoc}
1467      */
1468     @Override
1469     @JsxGetter
1470     public String getLocalName() {
1471         return super.getLocalName();
1472     }
1473 
1474     /**
1475      * {@inheritDoc}
1476      */
1477     @Override
1478     @JsxGetter
1479     public String getNamespaceURI() {
1480         return super.getNamespaceURI();
1481     }
1482 
1483     /**
1484      * Returns the {@code onbeforecopy} event handler for this element.
1485      *
1486      * @return the {@code onbeforecopy} event handler for this element
1487      */
1488     @JsxGetter({CHROME, EDGE})
1489     public Function getOnbeforecopy() {
1490         return getEventHandler(Event.TYPE_BEFORECOPY);
1491     }
1492 
1493     /**
1494      * Sets the {@code onbeforecopy} event handler for this element.
1495      *
1496      * @param onbeforecopy the {@code onbeforecopy} event handler for this element
1497      */
1498     @JsxSetter({CHROME, EDGE})
1499     public void setOnbeforecopy(final Object onbeforecopy) {
1500         setEventHandler(Event.TYPE_BEFORECOPY, onbeforecopy);
1501     }
1502 
1503     /**
1504      * Returns the {@code onbeforecut} event handler for this element.
1505      *
1506      * @return the {@code onbeforecut} event handler for this element
1507      */
1508     @JsxGetter({CHROME, EDGE})
1509     public Function getOnbeforecut() {
1510         return getEventHandler(Event.TYPE_BEFORECUT);
1511     }
1512 
1513     /**
1514      * Sets the {@code onbeforecut} event handler for this element.
1515      *
1516      * @param onbeforecut the {@code onbeforecut} event handler for this element
1517      */
1518     @JsxSetter({CHROME, EDGE})
1519     public void setOnbeforecut(final Object onbeforecut) {
1520         setEventHandler(Event.TYPE_BEFORECUT, onbeforecut);
1521     }
1522 
1523     /**
1524      * Returns the {@code onbeforepaste} event handler for this element.
1525      *
1526      * @return the {@code onbeforepaste} event handler for this element
1527      */
1528     @JsxGetter({CHROME, EDGE})
1529     public Function getOnbeforepaste() {
1530         return getEventHandler(Event.TYPE_BEFOREPASTE);
1531     }
1532 
1533     /**
1534      * Sets the {@code onbeforepaste} event handler for this element.
1535      *
1536      * @param onbeforepaste the {@code onbeforepaste} event handler for this element
1537      */
1538     @JsxSetter({CHROME, EDGE})
1539     public void setOnbeforepaste(final Object onbeforepaste) {
1540         setEventHandler(Event.TYPE_BEFOREPASTE, onbeforepaste);
1541     }
1542 
1543     /**
1544      * Returns the {@code onsearch} event handler for this element.
1545      *
1546      * @return the {@code onsearch} event handler for this element
1547      */
1548     @JsxGetter({CHROME, EDGE})
1549     public Function getOnsearch() {
1550         return getEventHandler(Event.TYPE_SEARCH);
1551     }
1552 
1553     /**
1554      * Sets the {@code onsearch} event handler for this element.
1555      *
1556      * @param onsearch the {@code onsearch} event handler for this element
1557      */
1558     @JsxSetter({CHROME, EDGE})
1559     public void setOnsearch(final Object onsearch) {
1560         setEventHandler(Event.TYPE_SEARCH, onsearch);
1561     }
1562 
1563     /**
1564      * Returns the {@code onwebkitfullscreenchange} event handler for this element.
1565      *
1566      * @return the {@code onwebkitfullscreenchange} event handler for this element
1567      */
1568     @JsxGetter({CHROME, EDGE})
1569     public Function getOnwebkitfullscreenchange() {
1570         return getEventHandler(Event.TYPE_WEBKITFULLSCREENCHANGE);
1571     }
1572 
1573     /**
1574      * Sets the {@code onwebkitfullscreenchange} event handler for this element.
1575      *
1576      * @param onwebkitfullscreenchange the {@code onwebkitfullscreenchange} event handler for this element
1577      */
1578     @JsxSetter({CHROME, EDGE})
1579     public void setOnwebkitfullscreenchange(final Object onwebkitfullscreenchange) {
1580         setEventHandler(Event.TYPE_WEBKITFULLSCREENCHANGE, onwebkitfullscreenchange);
1581     }
1582 
1583     /**
1584      * Returns the {@code onwebkitfullscreenerror} event handler for this element.
1585      *
1586      * @return the {@code onwebkitfullscreenerror} event handler for this element
1587      */
1588     @JsxGetter({CHROME, EDGE})
1589     public Function getOnwebkitfullscreenerror() {
1590         return getEventHandler(Event.TYPE_WEBKITFULLSCREENERROR);
1591     }
1592 
1593     /**
1594      * Sets the {@code onwebkitfullscreenerror} event handler for this element.
1595      *
1596      * @param onwebkitfullscreenerror the {@code onwebkitfullscreenerror} event handler for this element
1597      */
1598     @JsxSetter({CHROME, EDGE})
1599     public void setOnwebkitfullscreenerror(final Object onwebkitfullscreenerror) {
1600         setEventHandler(Event.TYPE_WEBKITFULLSCREENERROR, onwebkitfullscreenerror);
1601     }
1602 
1603     /**
1604      * Returns the {@code onwheel} event handler for this element.
1605      *
1606      * @return the {@code onwheel} event handler for this element
1607      */
1608     public Function getOnwheel() {
1609         return getEventHandler(Event.TYPE_WHEEL);
1610     }
1611 
1612     /**
1613      * Sets the {@code onwheel} event handler for this element.
1614      *
1615      * @param onwheel the {@code onwheel} event handler for this element
1616      */
1617     public void setOnwheel(final Object onwheel) {
1618         setEventHandler(Event.TYPE_WHEEL, onwheel);
1619     }
1620 
1621     /**
1622      * {@inheritDoc}
1623      */
1624     @Override
1625     @JsxFunction
1626     public void remove() {
1627         super.remove();
1628     }
1629 
1630     /**
1631      * Sets mouse capture to the object that belongs to the current document.
1632      * This is a mock implementation.
1633      *
1634      * @param retargetToElement if {@code true}, all events are targeted directly to this element;
1635      *        if {@code false}, events can also fire at descendants of this element
1636      */
1637     @JsxFunction({FF, FF_ESR})
1638     public void setCapture(final boolean retargetToElement) {
1639         // empty
1640     }
1641 
1642     /**
1643      * Releases mouse capture from the object in the current document.
1644      * This is a mock implementation.
1645      */
1646     @JsxFunction({FF, FF_ESR})
1647     public void releaseCapture() {
1648         // nothing to do
1649     }
1650 
1651     /**
1652      * Inserts a set of {@link Node} or {@code DOMString} objects in the children list of this node's parent,
1653      * just before this node.
1654      *
1655      * @param context the context
1656      * @param scope the scope
1657      * @param thisObj this object
1658      * @param args the arguments
1659      * @param function the function
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      * Inserts a set of {@link Node} or {@code DOMString} objects in the children list of this node's parent,
1669      * just after this node.
1670      *
1671      * @param context the context
1672      * @param scope the scope
1673      * @param thisObj this object
1674      * @param args the arguments
1675      * @param function the function
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      * Replaces this node with a set of {@link Node} or {@code DOMString} objects.
1685      *
1686      * @param context the context
1687      * @param scope the scope
1688      * @param thisObj this object
1689      * @param args the arguments
1690      * @param function the function
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      * Returns {@code true} if the element would be selected by the specified CSS selector string.
1700      *
1701      * @param context the JavaScript context
1702      * @param scope the scope
1703      * @param thisObj the scriptable
1704      * @param args the arguments passed into the method
1705      * @param function the function
1706      * @return {@code true} if the element matches the selector
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      * Returns {@code true} if the element would be selected by the specified CSS selector string.
1731      * Firefox-specific alias for {@link #matches}.
1732      *
1733      * @param context the JavaScript context
1734      * @param scope the scope
1735      * @param thisObj the scriptable
1736      * @param args the arguments passed into the method
1737      * @param function the function
1738      * @return {@code true} if the element matches the selector
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      * Returns {@code true} if the element would be selected by the specified CSS selector string.
1748      * WebKit-specific alias for {@link #matches}.
1749      *
1750      * @param context the JavaScript context
1751      * @param scope the scope
1752      * @param thisObj the scriptable
1753      * @param args the arguments passed into the method
1754      * @param function the function
1755      * @return {@code true} if the element matches the selector
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      * Traverses this element and its ancestors until it finds a node that matches the specified CSS selector.
1765      *
1766      * @param context the context
1767      * @param scope the scope
1768      * @param thisObj this object
1769      * @param args the arguments
1770      * @param function the function
1771      * @return the closest matching ancestor element, or {@code null} if none found
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      * Toggles a Boolean attribute on this element. If {@code force} is {@code true}, adds
1801      * the attribute. If {@code force} is {@code false}, removes the attribute.
1802      * If {@code force} is not specified, the attribute is toggled.
1803      *
1804      * @param name the name of the attribute to toggle; automatically converted to lower-case for HTML elements
1805      * @param force if {@code true}, adds the attribute; if {@code false}, removes it
1806      * @return {@code true} if the attribute is present after the call, {@code false} otherwise
1807      * @see <a href="https://developer.mozilla.org/en-US/docs/Web/API/Element/toggleAttribute">MDN Documentation</a>
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      * Inserts a set of {@link Node} objects or string objects after the last child of this element.
1829      * String objects are inserted as equivalent {@code Text} nodes.
1830      *
1831      * @param context the context
1832      * @param scope the scope
1833      * @param thisObj this object
1834      * @param args the arguments
1835      * @param function the function
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      * Inserts a set of {@link Node} objects or string objects before the first child of this element.
1849      * String objects are inserted as equivalent {@code Text} nodes.
1850      *
1851      * @param context the context
1852      * @param scope the scope
1853      * @param thisObj this object
1854      * @param args the arguments
1855      * @param function the function
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      * Replaces the existing children of this element with a specified new set of children.
1869      * These can be string or {@link Node} objects.
1870      *
1871      * @param context the context
1872      * @param scope the scope
1873      * @param thisObj this object
1874      * @param args the arguments
1875      * @param function the function
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 }