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