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