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