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