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.html;
16  
17  import static org.htmlunit.BrowserVersionFeatures.HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT;
18  import static org.htmlunit.BrowserVersionFeatures.KEYBOARD_EVENT_SPECIAL_KEYPRESS;
19  import static org.htmlunit.css.CssStyleSheet.ABSOLUTE;
20  import static org.htmlunit.css.CssStyleSheet.FIXED;
21  import static org.htmlunit.css.CssStyleSheet.STATIC;
22  
23  import java.io.IOException;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.Locale;
27  import java.util.Map;
28  
29  import org.htmlunit.BrowserVersion;
30  import org.htmlunit.ElementNotFoundException;
31  import org.htmlunit.Page;
32  import org.htmlunit.ScriptResult;
33  import org.htmlunit.SgmlPage;
34  import org.htmlunit.WebAssert;
35  import org.htmlunit.WebClient;
36  import org.htmlunit.WebWindow;
37  import org.htmlunit.css.ComputedCssStyleDeclaration;
38  import org.htmlunit.html.impl.SelectableTextInput;
39  import org.htmlunit.javascript.HtmlUnitScriptable;
40  import org.htmlunit.javascript.host.dom.Document;
41  import org.htmlunit.javascript.host.dom.MutationObserver;
42  import org.htmlunit.javascript.host.event.Event;
43  import org.htmlunit.javascript.host.event.EventTarget;
44  import org.htmlunit.javascript.host.event.KeyboardEvent;
45  import org.htmlunit.javascript.host.html.HTMLDocument;
46  import org.htmlunit.javascript.host.html.HTMLElement;
47  import org.htmlunit.util.StringUtils;
48  import org.w3c.dom.Attr;
49  import org.w3c.dom.CDATASection;
50  import org.w3c.dom.Comment;
51  import org.w3c.dom.DOMException;
52  import org.w3c.dom.Element;
53  import org.w3c.dom.EntityReference;
54  import org.w3c.dom.Node;
55  import org.w3c.dom.ProcessingInstruction;
56  import org.w3c.dom.Text;
57  
58  /**
59   * An abstract wrapper for HTML elements.
60   *
61   * @author Mike Bowler
62   * @author Mike J. Bresnahan
63   * @author David K. Taylor
64   * @author Christian Sell
65   * @author David D. Kilzer
66   * @author Mike Gallaher
67   * @author Denis N. Antonioli
68   * @author Marc Guillemot
69   * @author Ahmed Ashour
70   * @author Daniel Gredler
71   * @author Dmitri Zoubkov
72   * @author Sudhan Moghe
73   * @author Ronald Brill
74   * @author Frank Danek
75   * @author Ronny Shapiro
76   * @author Lai Quang Duong
77   */
78  public abstract class HtmlElement extends DomElement {
79  
80      /**
81       * Enum for the different display styles.
82       */
83      public enum DisplayStyle {
84          /** Empty string. */
85          EMPTY(""),
86          /** none. */
87          NONE("none"),
88          /** block. */
89          BLOCK("block"),
90          /** contents. */
91          CONTENTS("contents"),
92          /** inline. */
93          INLINE("inline"),
94          /** inline-block. */
95          INLINE_BLOCK("inline-block"),
96          /** list-item. */
97          LIST_ITEM("list-item"),
98          /** table. */
99          TABLE("table"),
100         /** table-cell. */
101         TABLE_CELL("table-cell"),
102         /** table-column. */
103         TABLE_COLUMN("table-column"),
104         /** table-column-group. */
105         TABLE_COLUMN_GROUP("table-column-group"),
106         /** table-row. */
107         TABLE_ROW("table-row"),
108         /** table-row-group. */
109         TABLE_ROW_GROUP("table-row-group"),
110         /** table-header-group. */
111         TABLE_HEADER_GROUP("table-header-group"),
112         /** table-footer-group. */
113         TABLE_FOOTER_GROUP("table-footer-group"),
114         /** table-caption. */
115         TABLE_CAPTION("table-caption"),
116         /** ruby. */
117         RUBY("ruby"),
118         /** ruby-base. */
119         RUBY_BASE("ruby-base"),
120         /** ruby-text-container. */
121         RUBY_TEXT("ruby-text"),
122         /** ruby-text-container. */
123         RUBY_TEXT_CONTAINER("ruby-text-container");
124 
125         private final String value_;
126         DisplayStyle(final String value) {
127             value_ = value;
128         }
129 
130         /**
131          * The string used from js.
132          * @return the value as string
133          */
134         public String value() {
135             return value_;
136         }
137     }
138 
139     /**
140      * Constant indicating that a tab index value is out of bounds (less than <code>0</code> or greater
141      * than <code>32767</code>).
142      *
143      * @see #getTabIndex()
144      */
145     public static final Short TAB_INDEX_OUT_OF_BOUNDS = Short.valueOf(Short.MIN_VALUE);
146 
147     /** Constant 'required'. */
148     protected static final String ATTRIBUTE_REQUIRED = "required";
149     /** Constant 'checked'. */
150     protected static final String ATTRIBUTE_CHECKED = "checked";
151     /** Constant 'hidden'. */
152     protected static final String ATTRIBUTE_HIDDEN = "hidden";
153     /** Constant 'readonly'. */
154     protected static final String ATTRIBUTE_READONLY = "readonly";
155 
156     /** The listeners which are to be notified of attribute changes. */
157     private final List<HtmlAttributeChangeListener> attributeListeners_ = new ArrayList<>();
158 
159     /** The owning form for lost form children. */
160     private HtmlForm owningForm_;
161 
162     private boolean shiftPressed_;
163     private boolean ctrlPressed_;
164     private boolean altPressed_;
165 
166     /**
167      * Creates an instance.
168      *
169      * @param qualifiedName the qualified name of the element type to instantiate
170      * @param page the page that contains this element
171      * @param attributes a map ready initialized with the attributes for this element, or
172      *        {@code null}. The map will be stored as is, not copied.
173      */
174     protected HtmlElement(final String qualifiedName, final SgmlPage page,
175             final Map<String, DomAttr> attributes) {
176         this(Html.XHTML_NAMESPACE, qualifiedName, page, attributes);
177     }
178 
179     /**
180      * Creates an instance of a DOM element that can have a namespace.
181      *
182      * @param namespaceURI the URI that identifies an XML namespace
183      * @param qualifiedName the qualified name of the element type to instantiate
184      * @param page the page that contains this element
185      * @param attributes a map ready initialized with the attributes for this element, or
186      *        {@code null}. The map will be stored as is, not copied.
187      */
188     protected HtmlElement(final String namespaceURI, final String qualifiedName, final SgmlPage page,
189             final Map<String, DomAttr> attributes) {
190         super(namespaceURI, qualifiedName, page, attributes);
191     }
192 
193     /**
194      * {@inheritDoc}
195      */
196     @Override
197     protected void setAttributeNS(final String namespaceURI, final String qualifiedName,
198             final String attributeValue, final boolean notifyAttributeChangeListeners,
199             final boolean notifyMutationObservers) {
200 
201         final HtmlPage htmlPage = getHtmlPageOrNull();
202 
203         // TODO: Clean up; this is a hack for HtmlElement living within an XmlPage.
204         if (htmlPage == null) {
205             super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
206                     notifyMutationObservers);
207             return;
208         }
209 
210         final String oldAttributeValue = getAttribute(qualifiedName);
211         final boolean mappedElement = isAttachedToPage()
212                 && (DomElement.NAME_ATTRIBUTE.equals(qualifiedName) || DomElement.ID_ATTRIBUTE.equals(qualifiedName));
213         if (mappedElement) {
214             // cast is safe here because isMappedElement checks for HtmlPage
215             htmlPage.removeMappedElement(this, false, false);
216         }
217 
218         final HtmlAttributeChangeEvent event;
219         if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
220             event = new HtmlAttributeChangeEvent(this, qualifiedName, attributeValue);
221         }
222         else {
223             event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
224         }
225 
226         super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
227                 notifyMutationObservers);
228 
229         if (notifyAttributeChangeListeners) {
230             notifyAttributeChangeListeners(event, this, oldAttributeValue, notifyMutationObservers);
231         }
232 
233         fireAttributeChangeImpl(event, htmlPage, mappedElement, oldAttributeValue);
234     }
235 
236     /**
237      * Recursively notifies all {@link HtmlAttributeChangeListener}s.
238      * @param event the event
239      * @param element the element
240      * @param oldAttributeValue the old attribute value
241      * @param notifyMutationObservers whether to notify {@link MutationObserver}s or not
242      */
243     protected static void notifyAttributeChangeListeners(final HtmlAttributeChangeEvent event,
244             final HtmlElement element, final String oldAttributeValue, final boolean notifyMutationObservers) {
245         final List<HtmlAttributeChangeListener> listeners = new ArrayList<>(element.attributeListeners_);
246         if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
247             synchronized (listeners) {
248                 for (final HtmlAttributeChangeListener listener : listeners) {
249                     if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
250                         listener.attributeAdded(event);
251                     }
252                 }
253             }
254         }
255         else {
256             synchronized (listeners) {
257                 for (final HtmlAttributeChangeListener listener : listeners) {
258                     if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
259                         listener.attributeReplaced(event);
260                     }
261                 }
262             }
263         }
264 
265         final DomNode parentNode = element.getParentNode();
266         if (parentNode instanceof HtmlElement htmlElement) {
267             notifyAttributeChangeListeners(event, htmlElement, oldAttributeValue, notifyMutationObservers);
268         }
269     }
270 
271     private void fireAttributeChangeImpl(final HtmlAttributeChangeEvent event,
272             final HtmlPage htmlPage, final boolean mappedElement, final String oldAttributeValue) {
273         if (mappedElement) {
274             htmlPage.addMappedElement(this, false);
275         }
276 
277         if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
278             fireHtmlAttributeAdded(event);
279             htmlPage.fireHtmlAttributeAdded(event);
280         }
281         else {
282             fireHtmlAttributeReplaced(event);
283             htmlPage.fireHtmlAttributeReplaced(event);
284         }
285     }
286 
287     /**
288      * Sets the specified attribute. This method may be overridden by subclasses
289      * which are interested in specific attribute value changes, but such methods <b>must</b>
290      * invoke <code>super.setAttributeNode()</code>, and <b>should</b> consider the value of the
291      * <code>cloning</code> parameter when deciding whether or not to execute custom logic.
292      *
293      * @param attribute the attribute to set
294      * @return {@inheritDoc}
295      */
296     @Override
297     public Attr setAttributeNode(final Attr attribute) {
298         final HtmlPage htmlPage = getHtmlPageOrNull();
299 
300         // TODO: Clean up; this is a hack for HtmlElement living within an XmlPage.
301         if (htmlPage == null) {
302             return super.setAttributeNode(attribute);
303         }
304 
305         final String qualifiedName = attribute.getName();
306         final String oldAttributeValue = getAttribute(qualifiedName);
307 
308         final boolean mappedElement = isAttachedToPage()
309                 && (DomElement.NAME_ATTRIBUTE.equals(qualifiedName)
310                         || DomElement.ID_ATTRIBUTE.equals(qualifiedName));
311         if (mappedElement) {
312             htmlPage.removeMappedElement(this, false, false);
313         }
314 
315         final HtmlAttributeChangeEvent event;
316         if (ATTRIBUTE_NOT_DEFINED == oldAttributeValue) {
317             event = new HtmlAttributeChangeEvent(this, qualifiedName, attribute.getValue());
318         }
319         else {
320             event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
321         }
322         notifyAttributeChangeListeners(event, this, oldAttributeValue, true);
323 
324         final Attr result = super.setAttributeNode(attribute);
325 
326         fireAttributeChangeImpl(event, htmlPage, mappedElement, oldAttributeValue);
327 
328         return result;
329     }
330 
331     /**
332      * Removes an attribute specified by name from this element.
333      * @param attributeName the attribute attributeName
334      */
335     @Override
336     public void removeAttribute(final String attributeName) {
337         final String value = getAttribute(attributeName);
338         if (ATTRIBUTE_NOT_DEFINED == value) {
339             return;
340         }
341 
342         final HtmlPage htmlPage = getHtmlPageOrNull();
343 
344         // TODO: Clean up; this is a hack for HtmlElement living within an XmlPage.
345         if (htmlPage == null) {
346             super.removeAttribute(attributeName);
347             return;
348         }
349 
350         final boolean mapped = DomElement.NAME_ATTRIBUTE.equals(attributeName)
351                                 || DomElement.ID_ATTRIBUTE.equals(attributeName);
352         if (mapped) {
353             htmlPage.removeMappedElement(this, false, false);
354         }
355 
356         super.removeAttribute(attributeName);
357 
358         if (mapped) {
359             htmlPage.addMappedElement(this, false);
360         }
361 
362         final HtmlAttributeChangeEvent event = new HtmlAttributeChangeEvent(this, attributeName, value);
363         fireHtmlAttributeRemoved(event);
364         htmlPage.fireHtmlAttributeRemoved(event);
365     }
366 
367     /**
368      * Support for reporting HTML attribute changes. This method can be called when an attribute
369      * has been added, and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
370      * registered {@link HtmlAttributeChangeListener}s.
371      * <p>
372      * Note that this method recursively calls this element's parent's
373      * {@link #fireHtmlAttributeAdded(HtmlAttributeChangeEvent)} method.
374      * </p>
375      *
376      * @param event the event
377      * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
378      */
379     protected void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) {
380         final DomNode parentNode = getParentNode();
381         if (parentNode instanceof HtmlElement element) {
382             element.fireHtmlAttributeAdded(event);
383         }
384     }
385 
386     /**
387      * Support for reporting HTML attribute changes. This method can be called when an attribute
388      * has been replaced, and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
389      * registered {@link HtmlAttributeChangeListener}s.
390      * <p>
391      * Note that this method recursively calls this element's parent's
392      * {@link #fireHtmlAttributeReplaced(HtmlAttributeChangeEvent)} method.
393      * </p>
394      *
395      * @param event the event
396      * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
397      */
398     protected void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) {
399         final DomNode parentNode = getParentNode();
400         if (parentNode instanceof HtmlElement element) {
401             element.fireHtmlAttributeReplaced(event);
402         }
403     }
404 
405     /**
406      * Support for reporting HTML attribute changes. This method can be called when an attribute
407      * has been removed, and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
408      * registered {@link HtmlAttributeChangeListener}s.
409      * <p>
410      * Note that this method recursively calls this element's parent's
411      * {@link #fireHtmlAttributeRemoved(HtmlAttributeChangeEvent)} method.
412      * </p>
413      *
414      * @param event the event
415      * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
416      */
417     protected void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) {
418         synchronized (attributeListeners_) {
419             for (final HtmlAttributeChangeListener listener : attributeListeners_) {
420                 listener.attributeRemoved(event);
421             }
422         }
423         final DomNode parentNode = getParentNode();
424         if (parentNode instanceof HtmlElement element) {
425             element.fireHtmlAttributeRemoved(event);
426         }
427     }
428 
429     /**
430      * Returns the same value as returned by {@link #getTagName()}.
431      *
432      * @return the same value as returned by {@link #getTagName()}
433      */
434     @Override
435     public String getNodeName() {
436         final String prefix = getPrefix();
437         if (prefix != null) {
438             // create string builder only if needed (performance)
439             final StringBuilder name = new StringBuilder(prefix.toLowerCase(Locale.ROOT))
440                 .append(':')
441                 .append(getLocalName().toLowerCase(Locale.ROOT));
442             return name.toString();
443         }
444         return getLocalName().toLowerCase(Locale.ROOT);
445     }
446 
447     /**
448      * Returns this element's tab index, if it has one. If the tab index is outside the
449      * valid range (less than <code>0</code> or greater than <code>32767</code>), this method
450      * returns {@link #TAB_INDEX_OUT_OF_BOUNDS}. If this element does not have
451      * a tab index, or its tab index is otherwise invalid, this method returns {@code null}.
452      *
453      * @return this element's tab index
454      */
455     public Short getTabIndex() {
456         final String index = getAttributeDirect("tabindex");
457         if (index == null || index.isEmpty()) {
458             return null;
459         }
460         try {
461             final long l = Long.parseLong(index);
462             if (l >= 0 && l <= Short.MAX_VALUE) {
463                 return Short.valueOf((short) l);
464             }
465             return TAB_INDEX_OUT_OF_BOUNDS;
466         }
467         catch (final NumberFormatException e) {
468             return null;
469         }
470     }
471 
472     /**
473      * Returns the first element with the specified tag name that is an ancestor to this element, or
474      * {@code null} if no such element is found.
475      * @param tagName the name of the tag searched (case insensitive)
476      * @return the first element with the specified tag name that is an ancestor to this element
477      */
478     public HtmlElement getEnclosingElement(final String tagName) {
479         final String tagNameLC = tagName.toLowerCase(Locale.ROOT);
480 
481         for (DomNode currentNode = getParentNode(); currentNode != null; currentNode = currentNode.getParentNode()) {
482             if (currentNode instanceof HtmlElement element && currentNode.getNodeName().equals(tagNameLC)) {
483                 return element;
484             }
485         }
486         return null;
487     }
488 
489     /**
490      * Returns the form which contains this element, or {@code null} if this element is not inside
491      * a form.
492      * @return the form which contains this element
493      */
494     public HtmlForm getEnclosingForm() {
495         final String formId = getAttribute("form");
496         if (ATTRIBUTE_NOT_DEFINED != formId) {
497             final Element formById = getPage().getElementById(formId);
498             if (formById instanceof HtmlForm form) {
499                 return form;
500             }
501             return null;
502         }
503 
504         if (owningForm_ != null) {
505             return owningForm_;
506         }
507         return (HtmlForm) getEnclosingElement("form");
508     }
509 
510     /**
511      * Returns the form which contains this element. If this element is not inside a form, this method
512      * throws an {@link IllegalStateException}.
513      * @return the form which contains this element
514      */
515     public HtmlForm getEnclosingFormOrDie() {
516         final HtmlForm form = getEnclosingForm();
517         if (form == null) {
518             throw new IllegalStateException("Element is not contained within a form: " + this);
519         }
520         return form;
521     }
522 
523     /**
524      * Simulates typing the specified text while this element has focus.
525      * Note that for some elements, typing '\n' submits the enclosed form.
526      * @param text the text you with to simulate typing
527      * @throws IOException If an IO error occurs
528      */
529     public void type(final String text) throws IOException {
530         for (final char ch : text.toCharArray()) {
531             type(ch);
532         }
533     }
534 
535     /**
536      * Simulates typing the specified character while this element has focus, returning the page contained
537      * by this element's window after typing. Note that it may or may not be the same as the original page,
538      * depending on the JavaScript event handlers, etc. Note also that for some elements, typing <code>'\n'</code>
539      * submits the enclosed form.
540      *
541      * @param c the character you wish to simulate typing
542      * @return the page that occupies this window after typing
543      * @throws IOException if an IO error occurs
544      */
545     public Page type(final char c) throws IOException {
546         return type(c, true);
547     }
548 
549     /**
550      * Simulates typing the specified character while this element has focus, returning the page contained
551      * by this element's window after typing. Note that it may or may not be the same as the original page,
552      * depending on the JavaScript event handlers, etc. Note also that for some elements, typing <code>'\n'</code>
553      * submits the enclosed form.
554      *
555      * @param c the character you wish to simulate typing
556      * @param lastType is this the last character to type
557      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
558      * @throws IOException if an IO error occurs
559      */
560     private Page type(final char c, final boolean lastType)
561         throws IOException {
562         if (isDisabledElementAndDisabled()) {
563             return getPage();
564         }
565 
566         // make enclosing window the current one
567         getPage().getWebClient().setCurrentWindow(getPage().getEnclosingWindow());
568 
569         final HtmlPage page = (HtmlPage) getPage();
570         if (page.getFocusedElement() != this) {
571             focus();
572         }
573         final boolean isShiftNeeded = KeyboardEvent.isShiftNeeded(c, shiftPressed_);
574 
575         final Event shiftDown;
576         final ScriptResult shiftDownResult;
577         if (isShiftNeeded) {
578             shiftDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, KeyboardEvent.DOM_VK_SHIFT,
579                     true, ctrlPressed_, altPressed_);
580             shiftDownResult = fireEvent(shiftDown);
581         }
582         else {
583             shiftDown = null;
584             shiftDownResult = null;
585         }
586 
587         final Event keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, c,
588                                                 shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
589         final ScriptResult keyDownResult = fireEvent(keyDown);
590 
591         if (!keyDown.isAborted(keyDownResult)) {
592             final Event keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, c,
593                     shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
594             final ScriptResult keyPressResult = fireEvent(keyPress);
595 
596             if ((shiftDown == null || !shiftDown.isAborted(shiftDownResult))
597                     && !keyPress.isAborted(keyPressResult)) {
598                 doType(c, lastType);
599             }
600         }
601 
602         final WebClient webClient = page.getWebClient();
603         if (this instanceof HtmlSelectableTextInput
604                 || this instanceof HtmlTextArea) {
605             fireEvent(new KeyboardEvent(this, Event.TYPE_INPUT, c,
606                                         shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_));
607         }
608 
609         HtmlElement eventSource = this;
610         if (!isAttachedToPage()) {
611             eventSource = page.getBody();
612         }
613 
614         if (eventSource != null) {
615             final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, c,
616                                                     shiftPressed_ || isShiftNeeded, ctrlPressed_, altPressed_);
617             eventSource.fireEvent(keyUp);
618 
619             if (isShiftNeeded) {
620                 final Event shiftUp = new KeyboardEvent(this, Event.TYPE_KEY_UP,
621                                         KeyboardEvent.DOM_VK_SHIFT,
622                                         false, ctrlPressed_, altPressed_);
623                 eventSource.fireEvent(shiftUp);
624             }
625         }
626 
627         final HtmlForm form = getEnclosingForm();
628         if (form != null && c == '\n' && isSubmittableByEnter()) {
629             for (final DomElement descendant : form.getDomElementDescendants()) {
630                 if (descendant instanceof HtmlSubmitInput) {
631                     return descendant.click();
632                 }
633             }
634 
635             form.submit((SubmittableElement) this);
636 
637             if (webClient.isJavaScriptEnabled()) {
638                 webClient.getJavaScriptEngine().processPostponedActions();
639             }
640         }
641 
642         return webClient.getCurrentWindow().getEnclosedPage();
643     }
644 
645     /**
646      * Simulates typing the specified key code while this element has focus, returning the page contained
647      * by this element's window after typing. Note that it may or may not be the same as the original page,
648      * depending on the JavaScript event handlers, etc.
649      * Note also that for some elements, typing <code>XXXXXXXXXXX</code>
650      * submits the enclosed form.
651      * <p>
652      * An example of predefined values is {@link KeyboardEvent#DOM_VK_PAGE_DOWN}.
653      * </p>
654      *
655      * @param keyCode the key code to simulate typing
656      * @return the page that occupies this window after typing
657      * @throws IOException in case of problems
658      */
659     public Page type(final int keyCode) throws IOException {
660         return type(keyCode, true, true, true, true);
661     }
662 
663     /**
664      * Simulates typing the specified {@link Keyboard} while this element has focus, returning the page contained
665      * by this element's window after typing. Note that it may or may not be the same as the original page,
666      * depending on the JavaScript event handlers, etc.
667      * Note also that for some elements, typing <code>XXXXXXXXXXX</code>
668      * submits the enclosed form.
669      *
670      * @param keyboard the keyboard
671      * @return the page that occupies this window after typing
672      * @throws IOException if an IO error occurs
673      */
674     public Page type(final Keyboard keyboard) throws IOException {
675         Page page = null;
676 
677         final List<Keyboard.KeyAction> keys = keyboard.getKeys();
678 
679         if (keyboard.isStartAtEnd()) {
680             if (this instanceof SelectableTextInput textInput) {
681                 textInput.setSelectionStart(textInput.getText().length());
682             }
683             else {
684                 final DomText domText = getDoTypeNode();
685                 if (domText != null) {
686                     domText.moveSelectionToEnd();
687                 }
688             }
689         }
690 
691         final int size = keys.size();
692         int i = 0;
693         for (final Keyboard.KeyAction keyAction : keys) {
694             i++;
695             final boolean lastType = i == size;
696 
697             if (keyAction instanceof Keyboard.TypedChar typedChar) {
698                 type(typedChar.ch(), lastType);
699             }
700             else if (keyAction instanceof Keyboard.KeyCodeAction keyCodeAction) {
701                 final int key = keyCodeAction.keyCode();
702                 final boolean pressed = keyCodeAction.pressed();
703 
704                 switch (key) {
705                     case KeyboardEvent.DOM_VK_SHIFT -> shiftPressed_ = pressed;
706                     case KeyboardEvent.DOM_VK_CONTROL -> ctrlPressed_ = pressed;
707                     case KeyboardEvent.DOM_VK_ALT -> altPressed_ = pressed;
708                     default -> { }
709                 }
710 
711                 if (pressed) {
712                     if (key == KeyboardEvent.DOM_VK_SHIFT
713                             || key == KeyboardEvent.DOM_VK_CONTROL
714                             || key == KeyboardEvent.DOM_VK_ALT) {
715                         page = type(key, true, false, false, lastType);
716                     }
717                     else {
718                         page = type(key, true, true, true, lastType);
719                     }
720                 }
721                 else {
722                     page = type(key, false, false, true, lastType);
723                 }
724             }
725         }
726 
727         return page;
728     }
729 
730     private Page type(final int keyCode,
731                     final boolean fireKeyDown, final boolean fireKeyPress, final boolean fireKeyUp,
732                     final boolean lastType) throws IOException {
733         if (isDisabledElementAndDisabled()) {
734             return getPage();
735         }
736 
737         final HtmlPage page = (HtmlPage) getPage();
738         if (page.getFocusedElement() != this) {
739             focus();
740         }
741 
742         final Event keyDown;
743         final ScriptResult keyDownResult;
744         if (fireKeyDown) {
745             keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, keyCode, shiftPressed_, ctrlPressed_, altPressed_);
746             keyDownResult = fireEvent(keyDown);
747         }
748         else {
749             keyDown = null;
750             keyDownResult = null;
751         }
752 
753         final BrowserVersion browserVersion = page.getWebClient().getBrowserVersion();
754 
755         final Event keyPress;
756         final ScriptResult keyPressResult;
757         if (fireKeyPress && browserVersion.hasFeature(KEYBOARD_EVENT_SPECIAL_KEYPRESS)) {
758             keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, keyCode,
759                     shiftPressed_, ctrlPressed_, altPressed_);
760 
761             keyPressResult = fireEvent(keyPress);
762         }
763         else {
764             keyPress = null;
765             keyPressResult = null;
766         }
767 
768         if (keyDown != null && !keyDown.isAborted(keyDownResult)
769                 && (keyPress == null || !keyPress.isAborted(keyPressResult))) {
770             doType(keyCode, lastType);
771         }
772 
773         if (this instanceof HtmlTextInput
774             || this instanceof HtmlTextArea
775             || this instanceof HtmlTelInput
776             || this instanceof HtmlNumberInput
777             || this instanceof HtmlSearchInput
778             || this instanceof HtmlPasswordInput) {
779             final Event input = new KeyboardEvent(this, Event.TYPE_INPUT, keyCode,
780                     shiftPressed_, ctrlPressed_, altPressed_);
781             fireEvent(input);
782         }
783 
784         if (fireKeyUp) {
785             final Event keyUp = new KeyboardEvent(this, Event.TYPE_KEY_UP, keyCode,
786                     shiftPressed_, ctrlPressed_, altPressed_);
787             fireEvent(keyUp);
788         }
789 
790         return page.getWebClient().getCurrentWindow().getEnclosedPage();
791     }
792 
793     /**
794      * Performs the effective type action, called after the keyPress event and before the keyUp event.
795      * @param c the character you with to simulate typing
796      * @param lastType is this the last character to type
797      */
798     protected void doType(final char c, final boolean lastType) {
799         final DomText domText = getDoTypeNode();
800         if (domText != null) {
801             domText.doType(c, this, lastType);
802         }
803     }
804 
805     /**
806      * Performs the effective type action, called after the keyPress event and before the keyUp event.
807      * <p>
808      * An example of predefined values is {@link KeyboardEvent#DOM_VK_PAGE_DOWN}.
809      * </p>
810      *
811      * @param keyCode the key code wish to simulate typing
812      * @param lastType is this the last to type
813      * @throws IOException in case of problems
814      */
815     protected void doType(final int keyCode, final boolean lastType) throws IOException {
816         final DomText domText = getDoTypeNode();
817         if (domText != null) {
818             domText.doType(keyCode, this, lastType);
819         }
820     }
821 
822     /**
823      * Returns the node to type into.
824      * @return the node
825      */
826     private DomText getDoTypeNode() {
827         final HTMLElement scriptElement = getScriptableObject();
828         if (scriptElement.isIsContentEditable()
829                 || "on".equals(((Document) scriptElement.getOwnerDocument()).getDesignMode())) {
830 
831             DomNode node = this;
832             while (node.getLastChild() != null) {
833                 node = node.getLastChild();
834             }
835 
836             if (node instanceof DomText text) {
837                 return text;
838             }
839 
840             final DomText domText = new DomText(getPage(), "");
841             appendChild(domText);
842             return domText;
843         }
844         return null;
845     }
846 
847     /**
848      * Called from {@link DoTypeProcessor}.
849      * @param newValue the new value
850      * @param notifyAttributeChangeListeners to notify the associated {@link HtmlAttributeChangeListener}s
851      */
852     protected void typeDone(final String newValue, final boolean notifyAttributeChangeListeners) {
853         // nothing
854     }
855 
856     /**
857      * Indicates if the provided character can be "typed" in the element.
858      * @param c the character
859      * @return {@code true} if it is accepted
860      */
861     protected boolean acceptChar(final char c) {
862         // This range is this is private use area
863         // see http://www.unicode.org/charts/PDF/UE000.pdf
864         return (c < '\uE000' || c > '\uF8FF')
865                 && (c == ' ' || c == '\t' || c == '\u3000' || c == '\u2006' || !Character.isWhitespace(c));
866     }
867 
868     /**
869      * Returns {@code true} if clicking Enter (ASCII 10, or '\n') should submit the enclosed form (if any).
870      * The default implementation returns {@code false}.
871      * @return {@code true} if clicking Enter should submit the enclosed form (if any)
872      */
873     protected boolean isSubmittableByEnter() {
874         return false;
875     }
876 
877     /**
878      * Searches for an element based on the specified criteria, returning the first element which matches
879      * said criteria. Only elements which are descendants of this element are included in the search.
880      *
881      * @param elementName the name of the element to search for
882      * @param attributeName the name of the attribute to search for
883      * @param attributeValue the value of the attribute to search for
884      * @param <E> the sub-element type
885      * @return the first element which matches the specified search criteria
886      * @throws ElementNotFoundException if no element matches the specified search criteria
887      */
888     public final <E extends HtmlElement> E getOneHtmlElementByAttribute(final String elementName,
889             final String attributeName,
890         final String attributeValue) throws ElementNotFoundException {
891 
892         WebAssert.notNull("elementName", elementName);
893         WebAssert.notNull("attributeName", attributeName);
894         WebAssert.notNull("attributeValue", attributeValue);
895 
896         final List<E> list = getElementsByAttribute(elementName, attributeName, attributeValue);
897 
898         if (list.isEmpty()) {
899             throw new ElementNotFoundException(elementName, attributeName, attributeValue);
900         }
901 
902         return list.get(0);
903     }
904 
905     /**
906      * Returns all elements which are descendants of this element and match the specified search criteria.
907      *
908      * @param elementName the name of the element to search for
909      * @param attributeName the name of the attribute to search for
910      * @param attributeValue the value of the attribute to search for
911      * @param <E> the sub-element type
912      * @return all elements which are descendants of this element and match the specified search criteria
913      */
914     @SuppressWarnings("unchecked")
915     public final <E extends HtmlElement> List<E> getElementsByAttribute(
916             final String elementName,
917             final String attributeName,
918             final String attributeValue) {
919 
920         final List<E> list = new ArrayList<>();
921         final String lowerCaseTagName = elementName.toLowerCase(Locale.ROOT);
922 
923         for (final HtmlElement next : getHtmlElementDescendants()) {
924             if (next.getTagName().equals(lowerCaseTagName)) {
925                 final String attValue = next.getAttribute(attributeName);
926                 if (attValue.equals(attributeValue)) {
927                     list.add((E) next);
928                 }
929             }
930         }
931         return list;
932     }
933 
934     /**
935      * Appends a child element to this HTML element with the specified tag name
936      * if this HTML element does not already have a child with that tag name.
937      * Returns the appended child element, or the first existent child element
938      * with the specified tag name if none was appended.
939      * @param tagName the tag name of the child to append
940      * @return the added child, or the first existing child if none was added
941      */
942     public final HtmlElement appendChildIfNoneExists(final String tagName) {
943         final HtmlElement child;
944         final List<HtmlElement> children = getStaticElementsByTagName(tagName);
945         if (children.isEmpty()) {
946             // Add a new child and return it.
947             child = (HtmlElement) ((HtmlPage) getPage()).createElement(tagName);
948             appendChild(child);
949         }
950         else {
951             // Return the first existing child.
952             child = children.get(0);
953         }
954         return child;
955     }
956 
957     /**
958      * Removes the <code>i</code>th child element with the specified tag name
959      * from all relationships, if possible.
960      * @param tagName the tag name of the child to remove
961      * @param i the index of the child to remove
962      */
963     public final void removeChild(final String tagName, final int i) {
964         final List<HtmlElement> children = getStaticElementsByTagName(tagName);
965         if (i >= 0 && i < children.size()) {
966             children.get(i).remove();
967         }
968     }
969 
970     /**
971      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
972      * Returns {@code true} if this element has any JavaScript functions that need to be executed when the
973      * specified event occurs.
974      * @param eventName the name of the event, such as "onclick" or "onblur", etc
975      * @return true if an event handler has been defined otherwise false
976      */
977     public final boolean hasEventHandlers(final String eventName) {
978         if (getPage().getWebClient().isJavaScriptEngineEnabled()) {
979             final HtmlUnitScriptable jsObj = getScriptableObject();
980             if (jsObj instanceof EventTarget target) {
981                 return target.hasEventHandlers(eventName);
982             }
983         }
984         return false;
985     }
986 
987     /**
988      * Adds an HtmlAttributeChangeListener to the listener list.
989      * The listener is registered for all attributes of this HtmlElement,
990      * as well as descendant elements.
991      *
992      * @param listener the attribute change listener to be added
993      * @see #removeHtmlAttributeChangeListener(HtmlAttributeChangeListener)
994      */
995     public void addHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
996         WebAssert.notNull("listener", listener);
997         synchronized (attributeListeners_) {
998             attributeListeners_.add(listener);
999         }
1000     }
1001 
1002     /**
1003      * Removes an HtmlAttributeChangeListener from the listener list.
1004      * This method should be used to remove HtmlAttributeChangeListener that were registered
1005      * for all attributes of this HtmlElement, as well as descendant elements.
1006      *
1007      * @param listener the attribute change listener to be removed
1008      * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
1009      */
1010     public void removeHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
1011         WebAssert.notNull("listener", listener);
1012         synchronized (attributeListeners_) {
1013             attributeListeners_.remove(listener);
1014         }
1015     }
1016 
1017     /**
1018      * {@inheritDoc}
1019      */
1020     @Override
1021     protected void checkChildHierarchy(final Node childNode) throws DOMException {
1022         if (!((childNode instanceof Element) || (childNode instanceof Text)
1023             || (childNode instanceof Comment) || (childNode instanceof ProcessingInstruction)
1024             || (childNode instanceof CDATASection) || (childNode instanceof EntityReference))) {
1025             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
1026                 "The Element may not have a child of this type: " + childNode.getNodeType());
1027         }
1028         super.checkChildHierarchy(childNode);
1029     }
1030 
1031     /**
1032      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1033      *
1034      * Allows the parser to connect to a form that is not a parent of this due to malformed HTML code
1035      * @param form the owning form
1036      */
1037     public void setOwningForm(final HtmlForm form) {
1038         owningForm_ = form;
1039     }
1040 
1041     /**
1042      * Indicates if the attribute names are case sensitive.
1043      * @return {@code false}
1044      */
1045     @Override
1046     protected boolean isAttributeCaseSensitive() {
1047         return false;
1048     }
1049 
1050     /**
1051      * Returns the value of the attribute {@code lang}. Refer to the
1052      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1053      * documentation for details on the use of this attribute.
1054      *
1055      * @return the value of the attribute {@code lang} or an empty string if that attribute isn't defined
1056      */
1057     public final String getLangAttribute() {
1058         return getAttributeDirect("lang");
1059     }
1060 
1061     /**
1062      * Returns the value of the attribute {@code xml:lang}. Refer to the
1063      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1064      * documentation for details on the use of this attribute.
1065      *
1066      * @return the value of the attribute {@code xml:lang} or an empty string if that attribute isn't defined
1067      */
1068     public final String getXmlLangAttribute() {
1069         return getAttribute("xml:lang");
1070     }
1071 
1072     /**
1073      * Returns the value of the attribute {@code dir}. Refer to the
1074      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1075      * documentation for details on the use of this attribute.
1076      *
1077      * @return the value of the attribute {@code dir} or an empty string if that attribute isn't defined
1078      */
1079     public final String getTextDirectionAttribute() {
1080         return getAttributeDirect("dir");
1081     }
1082 
1083     /**
1084      * Returns the value of the attribute {@code onclick}. Refer to the
1085      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1086      * documentation for details on the use of this attribute.
1087      *
1088      * @return the value of the attribute {@code onclick} or an empty string if that attribute isn't defined
1089      */
1090     public final String getOnClickAttribute() {
1091         return getAttributeDirect("onclick");
1092     }
1093 
1094     /**
1095      * Returns the value of the attribute {@code ondblclick}. Refer to the
1096      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1097      * documentation for details on the use of this attribute.
1098      *
1099      * @return the value of the attribute {@code ondblclick} or an empty string if that attribute isn't defined
1100      */
1101     public final String getOnDblClickAttribute() {
1102         return getAttributeDirect("ondblclick");
1103     }
1104 
1105     /**
1106      * Returns the value of the attribute {@code onmousedown}. Refer to the
1107      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1108      * documentation for details on the use of this attribute.
1109      *
1110      * @return the value of the attribute {@code onmousedown} or an empty string if that attribute isn't defined
1111      */
1112     public final String getOnMouseDownAttribute() {
1113         return getAttributeDirect("onmousedown");
1114     }
1115 
1116     /**
1117      * Returns the value of the attribute {@code onmouseup}. Refer to the
1118      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1119      * documentation for details on the use of this attribute.
1120      *
1121      * @return the value of the attribute {@code onmouseup} or an empty string if that attribute isn't defined
1122      */
1123     public final String getOnMouseUpAttribute() {
1124         return getAttributeDirect("onmouseup");
1125     }
1126 
1127     /**
1128      * Returns the value of the attribute {@code onmouseover}. Refer to the
1129      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1130      * documentation for details on the use of this attribute.
1131      *
1132      * @return the value of the attribute {@code onmouseover} or an empty string if that attribute isn't defined
1133      */
1134     public final String getOnMouseOverAttribute() {
1135         return getAttributeDirect("onmouseover");
1136     }
1137 
1138     /**
1139      * Returns the value of the attribute {@code onmousemove}. Refer to the
1140      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1141      * documentation for details on the use of this attribute.
1142      *
1143      * @return the value of the attribute {@code onmousemove} or an empty string if that attribute isn't defined
1144      */
1145     public final String getOnMouseMoveAttribute() {
1146         return getAttributeDirect("onmousemove");
1147     }
1148 
1149     /**
1150      * Returns the value of the attribute {@code onmouseout}. Refer to the
1151      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1152      * documentation for details on the use of this attribute.
1153      *
1154      * @return the value of the attribute {@code onmouseout} or an empty string if that attribute isn't defined
1155      */
1156     public final String getOnMouseOutAttribute() {
1157         return getAttributeDirect("onmouseout");
1158     }
1159 
1160     /**
1161      * Returns the value of the attribute {@code onkeypress}. Refer to the
1162      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1163      * documentation for details on the use of this attribute.
1164      *
1165      * @return the value of the attribute {@code onkeypress} or an empty string if that attribute isn't defined
1166      */
1167     public final String getOnKeyPressAttribute() {
1168         return getAttributeDirect("onkeypress");
1169     }
1170 
1171     /**
1172      * Returns the value of the attribute {@code onkeydown}. Refer to the
1173      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1174      * documentation for details on the use of this attribute.
1175      *
1176      * @return the value of the attribute {@code onkeydown} or an empty string if that attribute isn't defined
1177      */
1178     public final String getOnKeyDownAttribute() {
1179         return getAttributeDirect("onkeydown");
1180     }
1181 
1182     /**
1183      * Returns the value of the attribute {@code onkeyup}. Refer to the
1184      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
1185      * documentation for details on the use of this attribute.
1186      *
1187      * @return the value of the attribute {@code onkeyup} or an empty string if that attribute isn't defined
1188      */
1189     public final String getOnKeyUpAttribute() {
1190         return getAttributeDirect("onkeyup");
1191     }
1192 
1193     /**
1194      * {@inheritDoc}
1195      */
1196     @Override
1197     public String getCanonicalXPath() {
1198         final DomNode parent = getParentNode();
1199         if (parent.getNodeType() == DOCUMENT_NODE) {
1200             return "/" + getNodeName();
1201         }
1202         return parent.getCanonicalXPath() + '/' + getXPathToken();
1203     }
1204 
1205     /**
1206      * Returns the XPath token for this node only.
1207      */
1208     private String getXPathToken() {
1209         final DomNode parent = getParentNode();
1210         int total = 0;
1211         int nodeIndex = 0;
1212         for (final DomNode child : parent.getChildren()) {
1213             if (child.getNodeType() == ELEMENT_NODE && child.getNodeName().equals(getNodeName())) {
1214                 total++;
1215             }
1216             if (child == this) {
1217                 nodeIndex = total;
1218             }
1219         }
1220 
1221         if (nodeIndex == 1 && total == 1) {
1222             return getNodeName();
1223         }
1224         return getNodeName() + '[' + nodeIndex + ']';
1225     }
1226 
1227     /**
1228      * Returns the value of the 'hidden' attribute or an empty string if not set.
1229      *
1230      * @return the value of the 'hidden' attribute or an empty string if not set.
1231      */
1232     public String getHidden() {
1233         return getAttributeDirect(ATTRIBUTE_HIDDEN);
1234     }
1235 
1236     /**
1237      * Returns true if the hidden attribute is set.
1238      *
1239      * @return true if the hidden attribute is set.
1240      */
1241     public boolean isHidden() {
1242         return ATTRIBUTE_NOT_DEFINED != getAttributeDirect(ATTRIBUTE_HIDDEN);
1243     }
1244 
1245     /**
1246      * Sets the {@code hidden} property.
1247      * If the provided string is empty, the 'hidden' attribute will be removed.
1248      * If the provided string is 'until-found' then the attribute value will be 'until-found'.
1249      * For all other provided strings the attribute will be set to ''.
1250      * @see #setHidden(boolean)
1251      * @param hidden the {@code hidden} property
1252      */
1253     public void setHidden(final String hidden) {
1254         if ("until-found".equalsIgnoreCase(hidden)) {
1255             setAttribute(ATTRIBUTE_HIDDEN, "until-found");
1256             return;
1257         }
1258 
1259         if (StringUtils.isEmptyString(hidden)) {
1260             removeAttribute(ATTRIBUTE_HIDDEN);
1261             return;
1262         }
1263 
1264         setAttribute(ATTRIBUTE_HIDDEN, "");
1265     }
1266 
1267     /**
1268      * Sets the {@code hidden} property.
1269      * @param hidden the {@code hidden} property
1270      */
1271     public void setHidden(final boolean hidden) {
1272         if (hidden) {
1273             setAttribute(ATTRIBUTE_HIDDEN, "");
1274             return;
1275         }
1276 
1277         removeAttribute(ATTRIBUTE_HIDDEN);
1278     }
1279 
1280     /**
1281      * {@inheritDoc}
1282      * Overwritten to support the hidden attribute (html5).
1283      */
1284     @Override
1285     public boolean isDisplayed() {
1286         if (isHidden()) {
1287             return false;
1288         }
1289         return super.isDisplayed();
1290     }
1291 
1292     /**
1293      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1294      *
1295      * Returns the default display style.
1296      *
1297      * @return the default display style
1298      */
1299     public DisplayStyle getDefaultStyleDisplay() {
1300         return DisplayStyle.BLOCK;
1301     }
1302 
1303     /**
1304      * Helper for src retrieval and normalization.
1305      *
1306      * @return the value of the attribute {@code src} with all line breaks removed
1307      *         or an empty string if that attribute isn't defined.
1308      */
1309     protected final String getSrcAttributeNormalized() {
1310         final String attrib = getAttributeDirect(SRC_ATTRIBUTE);
1311         if (ATTRIBUTE_NOT_DEFINED == attrib) {
1312             return attrib;
1313         }
1314 
1315         return StringUtils.replaceChars(attrib, "\r\n", "");
1316     }
1317 
1318     /**
1319      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1320      *
1321      * Detach this node from all relationships with other nodes.
1322      * This is the first step of a move.
1323      */
1324     @Override
1325     protected void detach() {
1326         final SgmlPage page = getPage();
1327         if (!page.getWebClient().isJavaScriptEngineEnabled()) {
1328             super.detach();
1329             return;
1330         }
1331 
1332         final HtmlUnitScriptable document = page.getScriptableObject();
1333 
1334         if (document instanceof HTMLDocument doc) {
1335             final Object activeElement = doc.getActiveElement();
1336 
1337             if (activeElement == getScriptableObject()) {
1338                 if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1339                     ((HtmlPage) page).setFocusedElement(null);
1340                 }
1341                 else {
1342                     ((HtmlPage) page).setElementWithFocus(null);
1343                 }
1344             }
1345             else {
1346                 for (final DomNode child : getChildNodes()) {
1347                     if (activeElement == child.getScriptableObject()) {
1348                         if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1349                             ((HtmlPage) page).setFocusedElement(null);
1350                         }
1351                         else {
1352                             ((HtmlPage) page).setElementWithFocus(null);
1353                         }
1354 
1355                         break;
1356                     }
1357                 }
1358             }
1359         }
1360         super.detach();
1361     }
1362 
1363     /**
1364      * {@inheritDoc}
1365      */
1366     @Override
1367     public boolean handles(final Event event) {
1368         if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
1369             return this instanceof SubmittableElement || getTabIndex() != null;
1370         }
1371 
1372         if (isDisabledElementAndDisabled()) {
1373             return false;
1374         }
1375         return super.handles(event);
1376     }
1377 
1378     /**
1379      * Returns whether the {@code SHIFT} is currently pressed.
1380      * @return whether the {@code SHIFT} is currently pressed
1381      */
1382     protected boolean isShiftPressed() {
1383         return shiftPressed_;
1384     }
1385 
1386     /**
1387      * Returns whether the {@code CTRL} is currently pressed.
1388      * @return whether the {@code CTRL} is currently pressed
1389      */
1390     public boolean isCtrlPressed() {
1391         return ctrlPressed_;
1392     }
1393 
1394     /**
1395      * Returns whether the {@code ALT} is currently pressed.
1396      * @return whether the {@code ALT} is currently pressed
1397      */
1398     public boolean isAltPressed() {
1399         return altPressed_;
1400     }
1401 
1402     /**
1403      * Returns whether this element satisfies all form validation constraints set.
1404      * @return whether this element satisfies all form validation constraints set
1405      */
1406     public boolean isValid() {
1407         return !isRequiredSupported()
1408                 || ATTRIBUTE_NOT_DEFINED == getAttributeDirect(ATTRIBUTE_REQUIRED)
1409                 || !getAttributeDirect(VALUE_ATTRIBUTE).isEmpty();
1410     }
1411 
1412     /**
1413      * Returns whether this element supports the {@code required} constraint.
1414      * @return whether this element supports the {@code required} constraint
1415      */
1416     protected boolean isRequiredSupported() {
1417         return false;
1418     }
1419 
1420     /**
1421      * Returns the true if the required attribute is set.
1422      *
1423      * @return the true if the required attribute is set
1424      */
1425     public boolean isRequired() {
1426         return isRequiredSupported() && hasAttribute(ATTRIBUTE_REQUIRED);
1427     }
1428 
1429     /**
1430      * Returns the true if the required attribute is supported and set.
1431      *
1432      * @return the true if the required attribute is supported and set
1433      */
1434     public boolean isOptional() {
1435         return isRequiredSupported() && !hasAttribute(ATTRIBUTE_REQUIRED);
1436     }
1437 
1438     /**
1439      * Sets the {@code required} attribute.
1440      * @param required the new attribute value
1441      */
1442     public void setRequired(final boolean required) {
1443         if (isRequiredSupported()) {
1444             if (required) {
1445                 setAttribute(ATTRIBUTE_REQUIRED, ATTRIBUTE_REQUIRED);
1446             }
1447             else {
1448                 removeAttribute(ATTRIBUTE_REQUIRED);
1449             }
1450         }
1451     }
1452 
1453     /**
1454      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1455      *
1456      * @param returnNullIfFixed if position is 'fixed' return null
1457      * @return the offset parent {@link HtmlElement}
1458      */
1459     public HtmlElement getOffsetParentInternal(final boolean returnNullIfFixed) {
1460         if (getParentNode() == null) {
1461             return null;
1462         }
1463 
1464         final WebWindow webWindow = getPage().getEnclosingWindow();
1465         final ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1466         final String position = style.getPositionWithInheritance();
1467 
1468         if (returnNullIfFixed && FIXED.equals(position)) {
1469             return null;
1470         }
1471 
1472         final boolean staticPos = STATIC.equals(position);
1473 
1474         DomNode currentElement = this;
1475         while (currentElement != null) {
1476 
1477             final DomNode parentNode = currentElement.getParentNode();
1478             if (parentNode instanceof HtmlBody
1479                 || (staticPos && parentNode instanceof HtmlTableDataCell)
1480                 || (staticPos && parentNode instanceof HtmlTable)) {
1481                 return (HtmlElement) parentNode;
1482             }
1483 
1484             if (parentNode instanceof HtmlElement element) {
1485                 final ComputedCssStyleDeclaration parentStyle =
1486                         webWindow.getComputedStyle(element, null);
1487                 final String parentPosition = parentStyle.getPositionWithInheritance();
1488                 if (!STATIC.equals(parentPosition)) {
1489                     return element;
1490                 }
1491             }
1492 
1493             currentElement = currentElement.getParentNode();
1494         }
1495 
1496         return null;
1497     }
1498 
1499     /**
1500      * Returns this element's top offset, which is the calculated left position of this.
1501      *
1502      * @return this element's top offset, which is the calculated left position of this
1503      *         element relative to the <code>offsetParent</code>.
1504      */
1505     public int getOffsetTop() {
1506         if (this instanceof HtmlBody) {
1507             return 0;
1508         }
1509 
1510         int top = 0;
1511 
1512         // Add the offset for this node.
1513         final WebWindow webWindow = getPage().getEnclosingWindow();
1514         ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1515         top += style.getTop(true, false, false);
1516 
1517         // If this node is absolutely positioned, we're done.
1518         final String position = style.getPositionWithInheritance();
1519         if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
1520             return top;
1521         }
1522 
1523         final HtmlElement offsetParent = getOffsetParentInternal(false);
1524 
1525         // Add the offset for the ancestor nodes.
1526         DomNode parentNode = getParentNode();
1527         while (parentNode != null && parentNode != offsetParent) {
1528             if (parentNode instanceof HtmlElement element) {
1529                 style = webWindow.getComputedStyle(element, null);
1530                 top += style.getTop(false, true, true);
1531             }
1532             parentNode = parentNode.getParentNode();
1533         }
1534 
1535         if (offsetParent != null) {
1536             style = webWindow.getComputedStyle(this, null);
1537             final boolean thisElementHasTopMargin = style.getMarginTopValue() != 0;
1538 
1539             style = webWindow.getComputedStyle(offsetParent, null);
1540             if (!thisElementHasTopMargin) {
1541                 top += style.getMarginTopValue();
1542             }
1543             top += style.getPaddingTopValue();
1544         }
1545 
1546         return top;
1547     }
1548 
1549     /**
1550      * Returns this element's left offset, which is the calculated left position of this.
1551      *
1552      * @return this element's left offset, which is the calculated left position of this
1553      *         element relative to the <code>offsetParent</code>.
1554      */
1555     public int getOffsetLeft() {
1556         if (this instanceof HtmlBody) {
1557             return 0;
1558         }
1559 
1560         int left = 0;
1561 
1562         // Add the offset for this node.
1563         final WebWindow webWindow = getPage().getEnclosingWindow();
1564         ComputedCssStyleDeclaration style = webWindow.getComputedStyle(this, null);
1565         left += style.getLeft(true, false, false);
1566 
1567         // If this node is absolutely positioned, we're done.
1568         final String position = style.getPositionWithInheritance();
1569         if (ABSOLUTE.equals(position) || FIXED.equals(position)) {
1570             return left;
1571         }
1572 
1573         final HtmlElement offsetParent = getOffsetParentInternal(false);
1574 
1575         DomNode parentNode = getParentNode();
1576         while (parentNode != null && parentNode != offsetParent) {
1577             if (parentNode instanceof HtmlElement element) {
1578                 style = webWindow.getComputedStyle(element, null);
1579                 left += style.getLeft(true, true, true);
1580             }
1581             parentNode = parentNode.getParentNode();
1582         }
1583 
1584         if (offsetParent != null) {
1585             style = webWindow.getComputedStyle(offsetParent, null);
1586             left += style.getMarginLeftValue();
1587             left += style.getPaddingLeftValue();
1588         }
1589 
1590         return left;
1591     }
1592 
1593     /**
1594      * Returns this element's X position.
1595      * @return this element's X position
1596      */
1597     public int getPosX() {
1598         int cumulativeOffset = 0;
1599         final WebWindow webWindow = getPage().getEnclosingWindow();
1600 
1601         HtmlElement element = this;
1602         while (element != null) {
1603             cumulativeOffset += element.getOffsetLeft();
1604             if (element != this) {
1605                 final ComputedCssStyleDeclaration style =
1606                         webWindow.getComputedStyle(element, null);
1607                 cumulativeOffset += style.getBorderLeftValue();
1608             }
1609             element = element.getOffsetParentInternal(false);
1610         }
1611 
1612         return cumulativeOffset;
1613     }
1614 
1615     /**
1616      * Returns this element's Y position.
1617      * @return this element's Y position
1618      */
1619     public int getPosY() {
1620         int cumulativeOffset = 0;
1621         final WebWindow webWindow = getPage().getEnclosingWindow();
1622 
1623         HtmlElement element = this;
1624         while (element != null) {
1625             cumulativeOffset += element.getOffsetTop();
1626             if (element != this) {
1627                 final ComputedCssStyleDeclaration style =
1628                         webWindow.getComputedStyle(element, null);
1629                 cumulativeOffset += style.getBorderTopValue();
1630             }
1631             element = element.getOffsetParentInternal(false);
1632         }
1633 
1634         return cumulativeOffset;
1635     }
1636 
1637     /**
1638      * {@inheritDoc}
1639      */
1640     @Override
1641     public DomNode cloneNode(final boolean deep) {
1642         final HtmlElement newNode = (HtmlElement) super.cloneNode(deep);
1643         if (!deep) {
1644             synchronized (attributeListeners_) {
1645                 newNode.attributeListeners_.clear();
1646                 newNode.attributeListeners_.addAll(attributeListeners_);
1647             }
1648         }
1649 
1650         return newNode;
1651     }
1652 }