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.KeyboardEvent;
50  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLDocument;
51  import com.gargoylesoftware.htmlunit.javascript.host.html.HTMLElement;
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   */
71  public abstract class HtmlElement extends DomElement {
72  
73      /**
74       * Enum for the different display styles.
75       */
76      public enum DisplayStyle {
77          /** Empty string. */
78          EMPTY(""),
79          /** none. */
80          NONE("none"),
81          /** block. */
82          BLOCK("block"),
83          /** inline. */
84          INLINE("inline"),
85          /** inline-block. */
86          INLINE_BLOCK("inline-block"),
87          /** list-item. */
88          LIST_ITEM("list-item"),
89          /** table. */
90          TABLE("table"),
91          /** table-cell. */
92          TABLE_CELL("table-cell"),
93          /** table-column. */
94          TABLE_COLUMN("table-column"),
95          /** table-column-group. */
96          TABLE_COLUMN_GROUP("table-column-group"),
97          /** table-row. */
98          TABLE_ROW("table-row"),
99          /** table-row-group. */
100         TABLE_ROW_GROUP("table-row-group"),
101         /** table-header-group. */
102         TABLE_HEADER_GROUP("table-header-group"),
103         /** table-footer-group. */
104         TABLE_FOOTER_GROUP("table-footer-group"),
105         /** table-caption. */
106         TABLE_CAPTION("table-caption"),
107         /** ruby. */
108         RUBY("ruby"),
109         /** ruby-text. */
110         RUBY_TEXT("ruby-text");
111 
112         private final String value_;
113         DisplayStyle(final String value) {
114             value_ = value;
115         }
116 
117         /**
118          * The string used from js.
119          * @return the value as string
120          */
121         public String value() {
122             return value_;
123         }
124     }
125 
126     /**
127      * Constant indicating that a tab index value is out of bounds (less than <tt>0</tt> or greater
128      * than <tt>32767</tt>).
129      *
130      * @see #getTabIndex()
131      */
132     public static final Short TAB_INDEX_OUT_OF_BOUNDS = new Short(Short.MIN_VALUE);
133 
134     /** The listeners which are to be notified of attribute changes. */
135     private final Collection<HtmlAttributeChangeListener> attributeListeners_;
136 
137     /** The owning form for lost form children. */
138     private HtmlForm owningForm_;
139 
140     private boolean shiftPressed_;
141     private boolean ctrlPressed_;
142     private boolean altPressed_;
143 
144     /**
145      * Creates an instance.
146      *
147      * @param qualifiedName the qualified name of the element type to instantiate
148      * @param page the page that contains this element
149      * @param attributes a map ready initialized with the attributes for this element, or
150      * {@code null}. The map will be stored as is, not copied.
151      */
152     protected HtmlElement(final String qualifiedName, final SgmlPage page,
153             final Map<String, DomAttr> attributes) {
154         this(HTMLParser.XHTML_NAMESPACE, qualifiedName, page, attributes);
155     }
156 
157     /**
158      * Creates an instance of a DOM element that can have a namespace.
159      *
160      * @param namespaceURI the URI that identifies an XML namespace
161      * @param qualifiedName the qualified name of the element type to instantiate
162      * @param page the page that contains this element
163      * @param attributes a map ready initialized with the attributes for this element, or
164      * {@code null}. The map will be stored as is, not copied.
165      */
166     protected HtmlElement(final String namespaceURI, final String qualifiedName, final SgmlPage page,
167             final Map<String, DomAttr> attributes) {
168         super(namespaceURI, qualifiedName, page, attributes);
169         attributeListeners_ = new LinkedHashSet<>();
170     }
171 
172     /**
173      * {@inheritDoc}
174      */
175     @Override
176     protected void setAttributeNS(final String namespaceURI, final String qualifiedName,
177             final String attributeValue, final boolean notifyAttributeChangeListeners,
178             final boolean notifyMutationObservers) {
179 
180         // TODO: Clean up; this is a hack for HtmlElement living within an XmlPage.
181         if (null == getHtmlPageOrNull()) {
182             super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
183                     notifyMutationObservers);
184             return;
185         }
186 
187         final String oldAttributeValue = getAttribute(qualifiedName);
188         final HtmlPage htmlPage = (HtmlPage) getPage();
189         final boolean mappedElement = isAttachedToPage()
190                     && HtmlPage.isMappedElement(htmlPage, qualifiedName);
191         if (mappedElement) {
192             // cast is save here because isMappedElement checks for HtmlPage
193             htmlPage.removeMappedElement(this);
194         }
195 
196         final HtmlAttributeChangeEvent event;
197         if (oldAttributeValue == ATTRIBUTE_NOT_DEFINED) {
198             event = new HtmlAttributeChangeEvent(this, qualifiedName, attributeValue);
199         }
200         else {
201             event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
202         }
203 
204         super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
205                 notifyMutationObservers);
206 
207         if (notifyAttributeChangeListeners) {
208             notifyAttributeChangeListeners(event, this, oldAttributeValue, notifyMutationObservers);
209         }
210 
211         fireAttributeChangeImpl(event, htmlPage, mappedElement, qualifiedName, attributeValue, oldAttributeValue);
212     }
213 
214     /**
215      * Recursively notifies all {@link HtmlAttributeChangeListener}s.
216      * @param event the event
217      * @param element the element
218      * @param oldAttributeValue the old attribute value
219      * @param notifyMutationObservers whether to notify {@link MutationObserver}s or not
220      */
221     protected static void notifyAttributeChangeListeners(final HtmlAttributeChangeEvent event,
222             final HtmlElement element, final String oldAttributeValue, final boolean notifyMutationObservers) {
223         final Collection<HtmlAttributeChangeListener> listeners = element.attributeListeners_;
224         if (oldAttributeValue == ATTRIBUTE_NOT_DEFINED) {
225             synchronized (listeners) {
226                 for (final HtmlAttributeChangeListener listener : listeners) {
227                     if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
228                         listener.attributeAdded(event);
229                     }
230                 }
231             }
232         }
233         else {
234             synchronized (listeners) {
235                 for (final HtmlAttributeChangeListener listener : listeners) {
236                     if (notifyMutationObservers || !(listener instanceof MutationObserver)) {
237                         listener.attributeReplaced(event);
238                     }
239                 }
240             }
241         }
242         final DomNode parentNode = element.getParentNode();
243         if (parentNode instanceof HtmlElement) {
244             notifyAttributeChangeListeners(event, (HtmlElement) parentNode, oldAttributeValue, notifyMutationObservers);
245         }
246     }
247 
248     private void fireAttributeChangeImpl(final HtmlAttributeChangeEvent event,
249             final HtmlPage htmlPage, final boolean mappedElement,
250             final String qualifiedName, final String attributeValue, final String oldAttributeValue) {
251         if (mappedElement) {
252             htmlPage.addMappedElement(this);
253         }
254 
255         if (oldAttributeValue == ATTRIBUTE_NOT_DEFINED) {
256             fireHtmlAttributeAdded(event);
257             htmlPage.fireHtmlAttributeAdded(event);
258         }
259         else {
260             fireHtmlAttributeReplaced(event);
261             htmlPage.fireHtmlAttributeReplaced(event);
262         }
263     }
264 
265     /**
266      * Sets the specified attribute. This method may be overridden by subclasses
267      * which are interested in specific attribute value changes, but such methods <b>must</b>
268      * invoke <tt>super.setAttributeNode()</tt>, and <b>should</b> consider the value of the
269      * <tt>cloning</tt> parameter when deciding whether or not to execute custom logic.
270      *
271      * @param attribute the attribute to set
272      * @return {@inheritDoc}
273      */
274     @Override
275     public Attr setAttributeNode(final Attr attribute) {
276         final String qualifiedName = attribute.getName();
277         final String oldAttributeValue = getAttribute(qualifiedName);
278         final HtmlPage htmlPage = (HtmlPage) getPage();
279         final boolean mappedElement = isAttachedToPage()
280                     && HtmlPage.isMappedElement(htmlPage, qualifiedName);
281         if (mappedElement) {
282             // cast is save here because isMappedElement checks for HtmlPage
283             htmlPage.removeMappedElement(this);
284         }
285 
286         final HtmlAttributeChangeEvent event;
287         if (oldAttributeValue == ATTRIBUTE_NOT_DEFINED) {
288             event = new HtmlAttributeChangeEvent(this, qualifiedName, attribute.getValue());
289         }
290         else {
291             event = new HtmlAttributeChangeEvent(this, qualifiedName, oldAttributeValue);
292         }
293         notifyAttributeChangeListeners(event, this, oldAttributeValue, true);
294 
295         final Attr result = super.setAttributeNode(attribute);
296 
297         fireAttributeChangeImpl(event, htmlPage, mappedElement, qualifiedName, attribute.getValue(), oldAttributeValue);
298 
299         return result;
300     }
301 
302     /**
303      * Removes an attribute specified by name from this element.
304      * @param attributeName the attribute attributeName
305      */
306     @Override
307     public final void removeAttribute(final String attributeName) {
308         final String value = getAttribute(attributeName);
309         if (value == ATTRIBUTE_NOT_DEFINED) {
310             return;
311         }
312 
313         final HtmlPage htmlPage = getHtmlPageOrNull();
314         if (htmlPage != null) {
315             htmlPage.removeMappedElement(this);
316         }
317 
318         super.removeAttribute(attributeName);
319 
320         if (htmlPage != null) {
321             htmlPage.addMappedElement(this);
322 
323             final HtmlAttributeChangeEvent event = new HtmlAttributeChangeEvent(this, attributeName, value);
324             fireHtmlAttributeRemoved(event);
325             htmlPage.fireHtmlAttributeRemoved(event);
326         }
327     }
328 
329     /**
330      * Support for reporting HTML attribute changes. This method can be called when an attribute
331      * has been added and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
332      * registered {@link HtmlAttributeChangeListener}s.
333      *
334      * Note that this method recursively calls this element's parent's
335      * {@link #fireHtmlAttributeAdded(HtmlAttributeChangeEvent)} method.
336      *
337      * @param event the event
338      * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
339      */
340     protected void fireHtmlAttributeAdded(final HtmlAttributeChangeEvent event) {
341         final DomNode parentNode = getParentNode();
342         if (parentNode instanceof HtmlElement) {
343             ((HtmlElement) parentNode).fireHtmlAttributeAdded(event);
344         }
345     }
346 
347     /**
348      * Support for reporting HTML attribute changes. This method can be called when an attribute
349      * has been replaced and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
350      * registered {@link HtmlAttributeChangeListener}s.
351      *
352      * Note that this method recursively calls this element's parent's
353      * {@link #fireHtmlAttributeReplaced(HtmlAttributeChangeEvent)} method.
354      *
355      * @param event the event
356      * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
357      */
358     protected void fireHtmlAttributeReplaced(final HtmlAttributeChangeEvent event) {
359         final DomNode parentNode = getParentNode();
360         if (parentNode instanceof HtmlElement) {
361             ((HtmlElement) parentNode).fireHtmlAttributeReplaced(event);
362         }
363     }
364 
365     /**
366      * Support for reporting HTML attribute changes. This method can be called when an attribute
367      * has been removed and it will send the appropriate {@link HtmlAttributeChangeEvent} to any
368      * registered {@link HtmlAttributeChangeListener}s.
369      *
370      * Note that this method recursively calls this element's parent's
371      * {@link #fireHtmlAttributeRemoved(HtmlAttributeChangeEvent)} method.
372      *
373      * @param event the event
374      * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
375      */
376     protected void fireHtmlAttributeRemoved(final HtmlAttributeChangeEvent event) {
377         synchronized (attributeListeners_) {
378             for (final HtmlAttributeChangeListener listener : attributeListeners_) {
379                 listener.attributeRemoved(event);
380             }
381         }
382         final DomNode parentNode = getParentNode();
383         if (parentNode instanceof HtmlElement) {
384             ((HtmlElement) parentNode).fireHtmlAttributeRemoved(event);
385         }
386     }
387 
388     /**
389      * @return the same value as returned by {@link #getTagName()}
390      */
391     @Override
392     public String getNodeName() {
393         final String prefix = getPrefix();
394         if (prefix != null) {
395             // create string builder only if needed (performance)
396             final StringBuilder name = new StringBuilder(prefix.toLowerCase(Locale.ROOT));
397             name.append(':');
398             name.append(getLocalName().toLowerCase(Locale.ROOT));
399             return name.toString();
400         }
401         return getLocalName().toLowerCase(Locale.ROOT);
402     }
403 
404     /**
405      * Returns this element's tab index, if it has one. If the tab index is outside of the
406      * valid range (less than <tt>0</tt> or greater than <tt>32767</tt>), this method
407      * returns {@link #TAB_INDEX_OUT_OF_BOUNDS}. If this element does not have
408      * a tab index, or its tab index is otherwise invalid, this method returns {@code null}.
409      *
410      * @return this element's tab index
411      */
412     public Short getTabIndex() {
413         final String index = getAttribute("tabindex");
414         if (index == null || index.isEmpty()) {
415             return null;
416         }
417         try {
418             final long l = Long.parseLong(index);
419             if (l >= 0 && l <= Short.MAX_VALUE) {
420                 return Short.valueOf((short) l);
421             }
422             return TAB_INDEX_OUT_OF_BOUNDS;
423         }
424         catch (final NumberFormatException e) {
425             return null;
426         }
427     }
428 
429     /**
430      * Returns the first element with the specified tag name that is an ancestor to this element, or
431      * {@code null} if no such element is found.
432      * @param tagName the name of the tag searched (case insensitive)
433      * @return the first element with the specified tag name that is an ancestor to this element
434      */
435     public HtmlElement getEnclosingElement(final String tagName) {
436         final String tagNameLC = tagName.toLowerCase(Locale.ROOT);
437 
438         for (DomNode currentNode = getParentNode(); currentNode != null; currentNode = currentNode.getParentNode()) {
439             if (currentNode instanceof HtmlElement && currentNode.getNodeName().equals(tagNameLC)) {
440                 return (HtmlElement) currentNode;
441             }
442         }
443         return null;
444     }
445 
446     /**
447      * Returns the form which contains this element, or {@code null} if this element is not inside
448      * of a form.
449      * @return the form which contains this element
450      */
451     public HtmlForm getEnclosingForm() {
452         if (owningForm_ != null) {
453             return owningForm_;
454         }
455         return (HtmlForm) getEnclosingElement("form");
456     }
457 
458     /**
459      * Returns the form which contains this element. If this element is not inside a form, this method
460      * throws an {@link IllegalStateException}.
461      * @return the form which contains this element
462      */
463     public HtmlForm getEnclosingFormOrDie() {
464         final HtmlForm form = getEnclosingForm();
465         if (form == null) {
466             throw new IllegalStateException("Element is not contained within a form: " + this);
467         }
468         return form;
469     }
470 
471     /**
472      * Simulates typing the specified text while this element has focus.
473      * Note that for some elements, typing '\n' submits the enclosed form.
474      * @param text the text you with to simulate typing
475      * @exception IOException If an IO error occurs
476      */
477     public void type(final String text) throws IOException {
478         for (final char ch : text.toCharArray()) {
479             type(ch);
480         }
481     }
482 
483     /**
484      * Simulates typing the specified character while this element has focus, returning the page contained
485      * by this element's window after typing. Note that it may or may not be the same as the original page,
486      * depending on the JavaScript event handlers, etc. Note also that for some elements, typing <tt>'\n'</tt>
487      * submits the enclosed form.
488      *
489      * @param c the character you wish to simulate typing
490      * @return the page that occupies this window after typing
491      * @exception IOException if an IO error occurs
492      */
493     public Page type(final char c) throws IOException {
494         return type(c, false, true);
495     }
496 
497     /**
498      * Simulates typing the specified character while this element has focus, returning the page contained
499      * by this element's window after typing. Note that it may or may not be the same as the original page,
500      * depending on the JavaScript event handlers, etc. Note also that for some elements, typing <tt>'\n'</tt>
501      * submits the enclosed form.
502      *
503      * @param c the character you wish to simulate typing
504      * @param startAtEnd whether typing should start at the text end or not
505      * @param lastType is this the last character to type
506      * @return the page contained in the current window as returned by {@link WebClient#getCurrentWindow()}
507      * @exception IOException if an IO error occurs
508      */
509     private Page type(final char c, final boolean startAtEnd, final boolean lastType)
510         throws IOException {
511         if (this instanceof DisabledElement && ((DisabledElement) this).isDisabled()) {
512             return getPage();
513         }
514 
515         // make enclosing window the current one
516         getPage().getWebClient().setCurrentWindow(getPage().getEnclosingWindow());
517 
518         final HtmlPage page = (HtmlPage) getPage();
519         if (page.getFocusedElement() != this) {
520             focus();
521         }
522         final boolean isShiftNeeded = KeyboardEvent.isShiftNeeded(c, shiftPressed_);
523 
524         final Event shiftDown;
525         final ScriptResult shiftDownResult;
526         if (isShiftNeeded) {
527             shiftDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, KeyboardEvent.DOM_VK_SHIFT,
528                     true, ctrlPressed_, altPressed_);
529             shiftDownResult = fireEvent(shiftDown);
530         }
531         else {
532             shiftDown = null;
533             shiftDownResult = null;
534         }
535 
536         final Event keyDown = new KeyboardEvent(this, Event.TYPE_KEY_DOWN, c,
537                 shiftPressed_ | isShiftNeeded, ctrlPressed_, altPressed_);
538         final ScriptResult keyDownResult = fireEvent(keyDown);
539 
540         if (!keyDown.isAborted(keyDownResult)) {
541             final Event keyPress = new KeyboardEvent(this, Event.TYPE_KEY_PRESS, c,
542                     shiftPressed_ | isShiftNeeded, ctrlPressed_, altPressed_);
543             final ScriptResult keyPressResult = fireEvent(keyPress);
544 
545             if ((shiftDown == null || !shiftDown.isAborted(shiftDownResult))
546                     && !keyPress.isAborted(keyPressResult)) {
547                 doType(c, startAtEnd, lastType);
548             }
549         }
550 
551         final boolean nashorn = false;
552         final WebClient webClient = page.getWebClient();
553         if (this instanceof HtmlTextInput
554             || this instanceof HtmlTextArea
555             || this instanceof HtmlPasswordInput) {
556             fireKeyboardEvent(nashorn, Event.TYPE_INPUT, c, shiftPressed_ | isShiftNeeded);
557         }
558 
559         fireKeyboardEvent(nashorn, 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 boolean nashorn, 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         return ((HTMLElement) jsObj).hasEventHandlers(eventName);
933     }
934 
935     /**
936      * Adds an HtmlAttributeChangeListener to the listener list.
937      * The listener is registered for all attributes of this HtmlElement,
938      * as well as descendant elements.
939      *
940      * @param listener the attribute change listener to be added
941      * @see #removeHtmlAttributeChangeListener(HtmlAttributeChangeListener)
942      */
943     public void addHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
944         WebAssert.notNull("listener", listener);
945         synchronized (attributeListeners_) {
946             attributeListeners_.add(listener);
947         }
948     }
949 
950     /**
951      * Removes an HtmlAttributeChangeListener from the listener list.
952      * This method should be used to remove HtmlAttributeChangeListener that were registered
953      * for all attributes of this HtmlElement, as well as descendant elements.
954      *
955      * @param listener the attribute change listener to be removed
956      * @see #addHtmlAttributeChangeListener(HtmlAttributeChangeListener)
957      */
958     public void removeHtmlAttributeChangeListener(final HtmlAttributeChangeListener listener) {
959         WebAssert.notNull("listener", listener);
960         synchronized (attributeListeners_) {
961             attributeListeners_.remove(listener);
962         }
963     }
964 
965     /**
966      * {@inheritDoc}
967      */
968     @Override
969     protected void checkChildHierarchy(final Node childNode) throws DOMException {
970         if (!((childNode instanceof Element) || (childNode instanceof Text)
971             || (childNode instanceof Comment) || (childNode instanceof ProcessingInstruction)
972             || (childNode instanceof CDATASection) || (childNode instanceof EntityReference))) {
973             throw new DOMException(DOMException.HIERARCHY_REQUEST_ERR,
974                 "The Element may not have a child of this type: " + childNode.getNodeType());
975         }
976         super.checkChildHierarchy(childNode);
977     }
978 
979     void setOwningForm(final HtmlForm form) {
980         owningForm_ = form;
981     }
982 
983     /**
984      * Indicates if the attribute names are case sensitive.
985      * @return {@code false}
986      */
987     @Override
988     protected boolean isAttributeCaseSensitive() {
989         return false;
990     }
991 
992     /**
993      * Returns the value of the attribute {@code lang}. Refer to the
994      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
995      * documentation for details on the use of this attribute.
996      *
997      * @return the value of the attribute {@code lang} or an empty string if that attribute isn't defined
998      */
999     public final String getLangAttribute() {
1000         return getAttribute("lang");
1001     }
1002 
1003     /**
1004      * Returns the value of the attribute {@code xml:lang}. Refer to the
1005      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1006      * documentation for details on the use of this attribute.
1007      *
1008      * @return the value of the attribute {@code xml:lang} or an empty string if that attribute isn't defined
1009      */
1010     public final String getXmlLangAttribute() {
1011         return getAttribute("xml:lang");
1012     }
1013 
1014     /**
1015      * Returns the value of the attribute {@code dir}. Refer to the
1016      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1017      * documentation for details on the use of this attribute.
1018      *
1019      * @return the value of the attribute {@code dir} or an empty string if that attribute isn't defined
1020      */
1021     public final String getTextDirectionAttribute() {
1022         return getAttribute("dir");
1023     }
1024 
1025     /**
1026      * Returns the value of the attribute {@code onclick}. Refer to the
1027      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1028      * documentation for details on the use of this attribute.
1029      *
1030      * @return the value of the attribute {@code onclick} or an empty string if that attribute isn't defined
1031      */
1032     public final String getOnClickAttribute() {
1033         return getAttribute("onclick");
1034     }
1035 
1036     /**
1037      * Returns the value of the attribute {@code ondblclick}. Refer to the
1038      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1039      * documentation for details on the use of this attribute.
1040      *
1041      * @return the value of the attribute {@code ondblclick} or an empty string if that attribute isn't defined
1042      */
1043     public final String getOnDblClickAttribute() {
1044         return getAttribute("ondblclick");
1045     }
1046 
1047     /**
1048      * Returns the value of the attribute {@code onmousedown}. Refer to the
1049      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1050      * documentation for details on the use of this attribute.
1051      *
1052      * @return the value of the attribute {@code onmousedown} or an empty string if that attribute isn't defined
1053      */
1054     public final String getOnMouseDownAttribute() {
1055         return getAttribute("onmousedown");
1056     }
1057 
1058     /**
1059      * Returns the value of the attribute {@code onmouseup}. Refer to the
1060      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1061      * documentation for details on the use of this attribute.
1062      *
1063      * @return the value of the attribute {@code onmouseup} or an empty string if that attribute isn't defined
1064      */
1065     public final String getOnMouseUpAttribute() {
1066         return getAttribute("onmouseup");
1067     }
1068 
1069     /**
1070      * Returns the value of the attribute {@code onmouseover}. Refer to the
1071      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1072      * documentation for details on the use of this attribute.
1073      *
1074      * @return the value of the attribute {@code onmouseover} or an empty string if that attribute isn't defined
1075      */
1076     public final String getOnMouseOverAttribute() {
1077         return getAttribute("onmouseover");
1078     }
1079 
1080     /**
1081      * Returns the value of the attribute {@code onmousemove}. Refer to the
1082      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1083      * documentation for details on the use of this attribute.
1084      *
1085      * @return the value of the attribute {@code onmousemove} or an empty string if that attribute isn't defined
1086      */
1087     public final String getOnMouseMoveAttribute() {
1088         return getAttribute("onmousemove");
1089     }
1090 
1091     /**
1092      * Returns the value of the attribute {@code onmouseout}. Refer to the
1093      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1094      * documentation for details on the use of this attribute.
1095      *
1096      * @return the value of the attribute {@code onmouseout} or an empty string if that attribute isn't defined
1097      */
1098     public final String getOnMouseOutAttribute() {
1099         return getAttribute("onmouseout");
1100     }
1101 
1102     /**
1103      * Returns the value of the attribute {@code onkeypress}. Refer to the
1104      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1105      * documentation for details on the use of this attribute.
1106      *
1107      * @return the value of the attribute {@code onkeypress} or an empty string if that attribute isn't defined
1108      */
1109     public final String getOnKeyPressAttribute() {
1110         return getAttribute("onkeypress");
1111     }
1112 
1113     /**
1114      * Returns the value of the attribute {@code onkeydown}. Refer to the
1115      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1116      * documentation for details on the use of this attribute.
1117      *
1118      * @return the value of the attribute {@code onkeydown} or an empty string if that attribute isn't defined
1119      */
1120     public final String getOnKeyDownAttribute() {
1121         return getAttribute("onkeydown");
1122     }
1123 
1124     /**
1125      * Returns the value of the attribute {@code onkeyup}. Refer to the
1126      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
1127      * documentation for details on the use of this attribute.
1128      *
1129      * @return the value of the attribute {@code onkeyup} or an empty string if that attribute isn't defined
1130      */
1131     public final String getOnKeyUpAttribute() {
1132         return getAttribute("onkeyup");
1133     }
1134 
1135     /**
1136      * {@inheritDoc}
1137      */
1138     @Override
1139     public String getCanonicalXPath() {
1140         final DomNode parent = getParentNode();
1141         if (parent.getNodeType() == DOCUMENT_NODE) {
1142             return "/" + getNodeName();
1143         }
1144         return parent.getCanonicalXPath() + '/' + getXPathToken();
1145     }
1146 
1147     /**
1148      * Returns the XPath token for this node only.
1149      */
1150     private String getXPathToken() {
1151         final DomNode parent = getParentNode();
1152         int total = 0;
1153         int nodeIndex = 0;
1154         for (final DomNode child : parent.getChildren()) {
1155             if (child.getNodeType() == ELEMENT_NODE && child.getNodeName().equals(getNodeName())) {
1156                 total++;
1157             }
1158             if (child == this) {
1159                 nodeIndex = total;
1160             }
1161         }
1162 
1163         if (nodeIndex == 1 && total == 1) {
1164             return getNodeName();
1165         }
1166         return getNodeName() + '[' + nodeIndex + ']';
1167     }
1168 
1169     /**
1170      * {@inheritDoc}
1171      * Overwritten to support the hidden attribute (html5).
1172      */
1173     @Override
1174     public boolean isDisplayed() {
1175         if (ATTRIBUTE_NOT_DEFINED != getAttribute("hidden")) {
1176             return false;
1177         }
1178         return super.isDisplayed();
1179     }
1180 
1181     /**
1182      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1183      *
1184      * Returns the default display style.
1185      *
1186      * @return the default display style
1187      */
1188     public DisplayStyle getDefaultStyleDisplay() {
1189         return DisplayStyle.BLOCK;
1190     }
1191 
1192     /**
1193      * Helper for src retrieval and normalization.
1194      *
1195      * @return the value of the attribute {@code src} with all line breaks removed
1196      * or an empty string if that attribute isn't defined.
1197      */
1198     protected final String getSrcAttributeNormalized() {
1199         // at the moment StringUtils.replaceChars returns the org string
1200         // if nothing to replace was found but the doc implies, that we
1201         // can't trust on this in the future
1202         final String attrib = getAttribute("src");
1203         if (ATTRIBUTE_NOT_DEFINED == attrib) {
1204             return attrib;
1205         }
1206 
1207         return StringUtils.replaceChars(attrib, "\r\n", "");
1208     }
1209 
1210     /**
1211      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
1212      *
1213      * Detach this node from all relationships with other nodes.
1214      * This is the first step of an move.
1215      */
1216     @Override
1217     protected void detach() {
1218         final Object document = getPage().getScriptableObject();
1219 
1220         if (document instanceof HTMLDocument) {
1221             final HTMLDocument doc = (HTMLDocument) document;
1222             final Object activeElement = doc.getActiveElement();
1223 
1224             if (activeElement == getScriptableObject()) {
1225                 doc.setActiveElement(null);
1226                 if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1227                     ((HtmlPage) getPage()).setFocusedElement(null);
1228                 }
1229                 else {
1230                     ((HtmlPage) getPage()).setElementWithFocus(null);
1231                 }
1232             }
1233             else {
1234                 for (DomNode child : getChildNodes()) {
1235                     if (activeElement == child.getScriptableObject()) {
1236                         doc.setActiveElement(null);
1237                         if (hasFeature(HTMLELEMENT_REMOVE_ACTIVE_TRIGGERS_BLUR_EVENT)) {
1238                             ((HtmlPage) getPage()).setFocusedElement(null);
1239                         }
1240                         else {
1241                             ((HtmlPage) getPage()).setElementWithFocus(null);
1242                         }
1243 
1244                         break;
1245                     }
1246                 }
1247             }
1248         }
1249         super.detach();
1250     }
1251 
1252     /**
1253      * {@inheritDoc}
1254      */
1255     @Override
1256     public boolean handles(final Event event) {
1257         if (Event.TYPE_BLUR.equals(event.getType()) || Event.TYPE_FOCUS.equals(event.getType())) {
1258             return this instanceof SubmittableElement || getTabIndex() != null;
1259         }
1260 
1261         if (this instanceof DisabledElement && ((DisabledElement) this).isDisabled()) {
1262             return false;
1263         }
1264 
1265         return super.handles(event);
1266     }
1267 
1268     /**
1269      * Returns whether the {@code SHIFT} is currently pressed.
1270      * @return whether the {@code SHIFT} is currently pressed
1271      */
1272     protected boolean isShiftPressed() {
1273         return shiftPressed_;
1274     }
1275 
1276     /**
1277      * Returns whether the {@code CTRL} is currently pressed.
1278      * @return whether the {@code CTRL} is currently pressed
1279      */
1280     public boolean isCtrlPressed() {
1281         return ctrlPressed_;
1282     }
1283 
1284     /**
1285      * Returns whether the {@code ALT} is currently pressed.
1286      * @return whether the {@code ALT} is currently pressed
1287      */
1288     public boolean isAltPressed() {
1289         return altPressed_;
1290     }
1291 
1292     /**
1293      * Returns whether this element satisfies all form validation constraints set.
1294      * @return whether this element satisfies all form validation constraints set
1295      */
1296     public boolean isValid() {
1297         return !isRequiredSupported() || getAttribute("required") == ATTRIBUTE_NOT_DEFINED
1298                 || !getAttribute("value").isEmpty();
1299     }
1300 
1301     /**
1302      * Returns whether this element supports the {@code required} constraint.
1303      * @return whether this element supports the {@code required} constraint
1304      */
1305     protected boolean isRequiredSupported() {
1306         return false;
1307     }
1308 
1309     /**
1310      * Returns the {@code required} attribute.
1311      * @return the {@code required} attribute
1312      */
1313     public boolean isRequired() {
1314         return isRequiredSupported() && hasAttribute("required");
1315     }
1316 
1317     /**
1318      * Sets the {@code required} attribute.
1319      * @param required the new attribute value
1320      */
1321     public void setRequired(final boolean required) {
1322         if (isRequiredSupported()) {
1323             if (required) {
1324                 setAttribute("required", "required");
1325             }
1326             else {
1327                 removeAttribute("required");
1328             }
1329         }
1330     }
1331 
1332 }