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