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