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