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