View Javadoc
1   /*
2    * Copyright (c) 2002-2026 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.html;
16  
17  import java.util.ArrayList;
18  import java.util.Collections;
19  import java.util.List;
20  import java.util.Map;
21  
22  import org.htmlunit.ElementNotFoundException;
23  import org.htmlunit.Page;
24  import org.htmlunit.SgmlPage;
25  import org.htmlunit.WebAssert;
26  import org.htmlunit.javascript.host.event.Event;
27  import org.htmlunit.javascript.host.event.MouseEvent;
28  import org.htmlunit.util.NameValuePair;
29  import org.htmlunit.util.StringUtils;
30  import org.w3c.dom.Node;
31  
32  /**
33   * Wrapper for the HTML element "select".
34   *
35   * @author Mike Bowler
36   * @author Mike J. Bresnahan
37   * @author David K. Taylor
38   * @author Christian Sell
39   * @author David D. Kilzer
40   * @author Marc Guillemot
41   * @author Daniel Gredler
42   * @author Ahmed Ashour
43   * @author Ronald Brill
44   * @author Frank Danek
45   * @author Lai Quang Duong
46   */
47  public class HtmlSelect extends HtmlElement implements DisabledElement, SubmittableElement,
48                  LabelableElement, ValidatableElement {
49  
50      /** The HTML tag represented by this element. */
51      public static final String TAG_NAME = "select";
52  
53      /** What is the index of the HtmlOption which was last selected. */
54      private int lastSelectedIndex_ = -1;
55      private String customValidity_;
56  
57      /**
58       * Creates an instance.
59       *
60       * @param qualifiedName the qualified name of the element type to instantiate
61       * @param page the page that contains this element
62       * @param attributes the initial attributes
63       */
64      HtmlSelect(final String qualifiedName, final SgmlPage page,
65              final Map<String, DomAttr> attributes) {
66          super(qualifiedName, page, attributes);
67      }
68  
69      /**
70       * If we were given an invalid <code>size</code> attribute, normalize it.
71       * Then set a default selected option if none was specified and the size is 1 or less
72       * and this isn't a multiple selection input.
73       * @param postponed whether to use {@link org.htmlunit.javascript.PostponedAction} or no
74       */
75      @Override
76      public void onAllChildrenAddedToPage(final boolean postponed) {
77          // Fix the size if necessary.
78          int size;
79          try {
80              size = Integer.parseInt(getSizeAttribute());
81              if (size < 0) {
82                  removeAttribute("size");
83                  size = 0;
84              }
85          }
86          catch (final NumberFormatException e) {
87              removeAttribute("size");
88              size = 0;
89          }
90  
91          // Set a default selected option if necessary.
92          if (getSelectedOptions().isEmpty() && size <= 1 && !isMultipleSelectEnabled()) {
93              final List<HtmlOption> options = getOptions();
94              if (!options.isEmpty()) {
95                  final HtmlOption first = options.get(0);
96                  first.setSelectedInternal(true);
97              }
98          }
99      }
100 
101     /**
102      * {@inheritDoc}
103      */
104     @Override
105     public boolean handles(final Event event) {
106         if (event instanceof MouseEvent) {
107             return true;
108         }
109 
110         return super.handles(event);
111     }
112 
113     /**
114      * <p>Returns all the currently selected options. The following special
115      * conditions can occur if the element is in single select mode:</p>
116      * <ul>
117      *   <li>if multiple options are erroneously selected, the last one is returned</li>
118      *   <li>if no options are selected, the first one is returned</li>
119      * </ul>
120      *
121      * @return the currently selected options
122      */
123     public List<HtmlOption> getSelectedOptions() {
124         final List<HtmlOption> result;
125         if (isMultipleSelectEnabled()) {
126             // Multiple selections possible.
127             result = new ArrayList<>();
128             for (final HtmlElement element : getHtmlElementDescendants()) {
129                 if (element instanceof HtmlOption option && option.isSelected()) {
130                     result.add(option);
131                 }
132             }
133         }
134         else {
135             // Only a single selection is possible.
136             result = new ArrayList<>(1);
137             HtmlOption lastSelected = null;
138             for (final HtmlElement element : getHtmlElementDescendants()) {
139                 if (element instanceof HtmlOption option) {
140                     if (option.isSelected()) {
141                         lastSelected = option;
142                     }
143                 }
144             }
145             if (lastSelected != null) {
146                 result.add(lastSelected);
147             }
148         }
149         return Collections.unmodifiableList(result);
150     }
151 
152     /**
153      * Returns all the options in this select element.
154      * @return all the options in this select element
155      */
156     public List<HtmlOption> getOptions() {
157         return Collections.unmodifiableList(getStaticElementsByTagName("option"));
158     }
159 
160     /**
161      * Returns the indexed option.
162      *
163      * @param index the index
164      * @return the option specified by the index
165      */
166     public HtmlOption getOption(final int index) {
167         return this.<HtmlOption>getStaticElementsByTagName("option").get(index);
168     }
169 
170     /**
171      * Returns the number of options.
172      * @return the number of options
173      */
174     public int getOptionSize() {
175         return getStaticElementsByTagName("option").size();
176     }
177 
178     /**
179      * Remove options by reducing the "length" property. This has no
180      * effect if the length is set to the same or greater.
181      * @param newLength the new length property value
182      */
183     public void setOptionSize(final int newLength) {
184         final List<HtmlElement> elementList = getStaticElementsByTagName("option");
185 
186         for (int i = elementList.size() - 1; i >= newLength; i--) {
187             elementList.get(i).remove();
188         }
189     }
190 
191     /**
192      * Remove an option at the given index.
193      * @param index the index of the option to remove
194      */
195     public void removeOption(final int index) {
196         final ChildElementsIterator iterator = new ChildElementsIterator(this);
197         int i = 0;
198         while (iterator.hasNext()) {
199             final DomElement element = iterator.next();
200             if (element instanceof HtmlOption) {
201                 if (i == index) {
202                     element.remove();
203                     ensureSelectedIndex();
204                     return;
205                 }
206                 i++;
207             }
208         }
209     }
210 
211     /**
212      * Replace an option at the given index with a new option.
213      * @param index the index of the option to remove
214      * @param newOption the new option to replace to indexed option
215      */
216     public void replaceOption(final int index, final HtmlOption newOption) {
217         final ChildElementsIterator iterator = new ChildElementsIterator(this);
218         int i = 0;
219         while (iterator.hasNext()) {
220             final DomElement element = iterator.next();
221             if (element instanceof HtmlOption) {
222                 if (i == index) {
223                     element.replace(newOption);
224 
225                     if (newOption.isSelected() && !isMultipleSelectEnabled()) {
226                         setOnlySelected(newOption, true);
227                     }
228                     ensureSelectedIndex();
229 
230                     return;
231                 }
232                 i++;
233             }
234         }
235     }
236 
237     /**
238      * Add a new option at the end.
239      * @param newOption the new option to add
240      */
241     public void appendOption(final HtmlOption newOption) {
242         appendChild(newOption);
243 
244         ensureSelectedIndex();
245     }
246 
247     /**
248      * {@inheritDoc}
249      */
250     @Override
251     public DomNode appendChild(final Node node) {
252         final DomNode response = super.appendChild(node);
253         if (node instanceof HtmlOption option) {
254             if (option.isSelected()) {
255                 doSelectOption(option, true, false, false, false);
256             }
257         }
258         return response;
259     }
260 
261     /**
262      * Sets the "selected" state of the specified option. If this "select" element
263      * is single-select, then calling this method will deselect all other options.
264      * <p>
265      * Only options that are actually in the document may be selected.
266      * </p>
267      *
268      * @param isSelected true if the option is to become selected
269      * @param optionValue the value of the option that is to change
270      * @param <P> the page type
271      * @return the page contained in the current window as returned
272      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
273      */
274     public <P extends Page> P setSelectedAttribute(final String optionValue, final boolean isSelected) {
275         return setSelectedAttribute(optionValue, isSelected, true);
276     }
277 
278     /**
279      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
280      *
281      * Sets the "selected" state of the specified option. If this "select" element
282      * is single-select, then calling this method will deselect all other options.
283      * <p>
284      * Only options that are actually in the document may be selected.
285      * </p>
286      *
287      * @param isSelected true if the option is to become selected
288      * @param optionValue the value of the option that is to change
289      * @param invokeOnFocus whether to set focus or not.
290      * @param <P> the page type
291      * @return the page contained in the current window as returned
292      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
293      */
294     @SuppressWarnings("unchecked")
295     public <P extends Page> P setSelectedAttribute(final String optionValue,
296             final boolean isSelected, final boolean invokeOnFocus) {
297         try {
298             final HtmlOption selected = getOptionByValue(optionValue);
299             return setSelectedAttribute(selected, isSelected, invokeOnFocus, true, false, true);
300         }
301         catch (final ElementNotFoundException e) {
302             for (final HtmlOption o : getSelectedOptions()) {
303                 o.setSelected(false);
304             }
305             return (P) getPage();
306         }
307     }
308 
309     /**
310      * Sets the "selected" state of the specified option. If this "select" element
311      * is single-select, then calling this method will deselect all other options.
312      * <p>
313      * Only options that are actually in the document may be selected.
314      * </p>
315      *
316      * @param isSelected true if the option is to become selected
317      * @param selectedOption the value of the option that is to change
318      * @param <P> the page type
319      * @return the page contained in the current window as returned
320      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
321      */
322     public <P extends Page> P setSelectedAttribute(final HtmlOption selectedOption, final boolean isSelected) {
323         return setSelectedAttribute(selectedOption, isSelected, true, true, false, true);
324     }
325 
326     /**
327      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
328      *
329      * Sets the "selected" state of the specified option. If this "select" element
330      * is single-select, then calling this method will deselect all other options.
331      * <p>
332      * Only options that are actually in the document may be selected.
333      * </p>
334      *
335      * @param isSelected true if the option is to become selected
336      * @param selectedOption the value of the option that is to change
337      * @param invokeOnFocus whether to set focus or not.
338      * @param shiftKey {@code true} if SHIFT is pressed
339      * @param ctrlKey {@code true} if CTRL is pressed
340      * @param isClick is mouse clicked
341      * @param <P> the page type
342      * @return the page contained in the current window as returned
343      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
344      */
345     @SuppressWarnings("unchecked")
346     public <P extends Page> P setSelectedAttribute(final HtmlOption selectedOption, final boolean isSelected,
347         final boolean invokeOnFocus, final boolean shiftKey, final boolean ctrlKey, final boolean isClick) {
348         if (isSelected && invokeOnFocus) {
349             ((HtmlPage) getPage()).setFocusedElement(this);
350         }
351 
352         final boolean changeSelectedState = selectedOption.isSelected() != isSelected;
353 
354         if (changeSelectedState) {
355             doSelectOption(selectedOption, isSelected, shiftKey, ctrlKey, isClick);
356             HtmlInput.executeOnChangeHandlerIfAppropriate(this);
357         }
358 
359         return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
360     }
361 
362     private void doSelectOption(final HtmlOption selectedOption,
363             final boolean isSelected, final boolean shiftKey, final boolean ctrlKey, final boolean isClick) {
364         // caution the HtmlOption may have been created from js and therefore the select now need
365         // to "know" that it is selected
366         if (isMultipleSelectEnabled()) {
367             selectedOption.setSelectedInternal(isSelected);
368             if (isClick && !ctrlKey) {
369                 if (!shiftKey) {
370                     setOnlySelected(selectedOption, isSelected);
371                     lastSelectedIndex_ = getOptions().indexOf(selectedOption);
372                 }
373                 else if (isSelected && lastSelectedIndex_ != -1) {
374                     final List<HtmlOption> options = getOptions();
375                     final int newIndex = options.indexOf(selectedOption);
376                     for (int i = 0; i < options.size(); i++) {
377                         options.get(i).setSelectedInternal(isBetween(i, lastSelectedIndex_, newIndex));
378                     }
379                 }
380             }
381         }
382         else {
383             setOnlySelected(selectedOption, isSelected);
384         }
385     }
386 
387     /**
388      * Sets the given {@link HtmlOption} as the only selected one.
389      * @param selectedOption the selected {@link HtmlOption}
390      * @param isSelected whether selected or not
391      */
392     void setOnlySelected(final HtmlOption selectedOption, final boolean isSelected) {
393         for (final HtmlOption option : getOptions()) {
394             option.setSelectedInternal(option == selectedOption && isSelected);
395         }
396     }
397 
398     private static boolean isBetween(final int number, final int min, final int max) {
399         return max > min ? number >= min && number <= max : number >= max && number <= min;
400     }
401 
402     /**
403      * {@inheritDoc}
404      */
405     @Override
406     public NameValuePair[] getSubmitNameValuePairs() {
407         final String name = getNameAttribute();
408 
409         final List<HtmlOption> selectedOptions = getSelectedOptions();
410 
411         final NameValuePair[] pairs = new NameValuePair[selectedOptions.size()];
412 
413         int i = 0;
414         for (final HtmlOption option : selectedOptions) {
415             pairs[i++] = new NameValuePair(name, option.getValueAttribute());
416         }
417         return pairs;
418     }
419 
420     /**
421      * Indicates if this select is submittable.
422      * @return {@code false} if not
423      */
424     boolean isValidForSubmission() {
425         return getOptionSize() > 0;
426     }
427 
428     /**
429      * Returns the value of this element to what it was at the time the page was loaded.
430      */
431     @Override
432     public void reset() {
433         for (final HtmlOption option : getOptions()) {
434             option.reset();
435         }
436         onAllChildrenAddedToPage(false);
437     }
438 
439     /**
440      * {@inheritDoc}
441      * @see SubmittableElement#setDefaultValue(String)
442      */
443     @Override
444     public void setDefaultValue(final String defaultValue) {
445         setSelectedAttribute(defaultValue, true);
446     }
447 
448     /**
449      * {@inheritDoc}
450      * @see SubmittableElement#setDefaultValue(String)
451      */
452     @Override
453     public String getDefaultValue() {
454         final List<HtmlOption> options = getSelectedOptions();
455         if (options.isEmpty()) {
456             return "";
457         }
458         return options.get(0).getValueAttribute();
459     }
460 
461     /**
462      * {@inheritDoc}
463      * This implementation is empty; only checkboxes and radio buttons
464      * really care what the default checked value is.
465      * @see SubmittableElement#setDefaultChecked(boolean)
466      * @see HtmlRadioButtonInput#setDefaultChecked(boolean)
467      * @see HtmlCheckBoxInput#setDefaultChecked(boolean)
468      */
469     @Override
470     public void setDefaultChecked(final boolean defaultChecked) {
471         // Empty.
472     }
473 
474     /**
475      * {@inheritDoc}
476      * This implementation returns {@code false}; only checkboxes and
477      * radio buttons really care what the default checked value is.
478      * @see SubmittableElement#isDefaultChecked()
479      * @see HtmlRadioButtonInput#isDefaultChecked()
480      * @see HtmlCheckBoxInput#isDefaultChecked()
481      */
482     @Override
483     public boolean isDefaultChecked() {
484         return false;
485     }
486 
487     /**
488      * Returns {@code true} if this select is using "multiple select".
489      * @return {@code true} if this select is using "multiple select"
490      */
491     public boolean isMultipleSelectEnabled() {
492         return getAttributeDirect("multiple") != ATTRIBUTE_NOT_DEFINED;
493     }
494 
495     /**
496      * Returns the {@link HtmlOption} object that corresponds to the specified value.
497      *
498      * @param value the value to search by
499      * @return the {@link HtmlOption} object that corresponds to the specified value
500      * @exception ElementNotFoundException If a particular element could not be found in the DOM model
501      */
502     public HtmlOption getOptionByValue(final String value) throws ElementNotFoundException {
503         WebAssert.notNull(VALUE_ATTRIBUTE, value);
504         for (final HtmlOption option : getOptions()) {
505             if (option.getValueAttribute().equals(value)) {
506                 return option;
507             }
508         }
509         throw new ElementNotFoundException("option", VALUE_ATTRIBUTE, value);
510     }
511 
512     /**
513      * Returns the {@link HtmlOption} object that has the specified text.
514      *
515      * @param text the text to search by
516      * @return the {@link HtmlOption} object that has the specified text
517      * @exception ElementNotFoundException If a particular element could not be found in the DOM model
518      */
519     public HtmlOption getOptionByText(final String text) throws ElementNotFoundException {
520         WebAssert.notNull("text", text);
521         for (final HtmlOption option : getOptions()) {
522             if (option.getText().equals(text)) {
523                 return option;
524             }
525         }
526         throw new ElementNotFoundException("option", "text", text);
527     }
528 
529     /**
530      * Returns the value of the attribute {@code name}. Refer to the <a
531      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
532      *
533      * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
534      */
535     public final String getNameAttribute() {
536         return getAttributeDirect(NAME_ATTRIBUTE);
537     }
538 
539     /**
540      * Returns the value of the attribute {@code size}. Refer to the <a
541      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for
542      * details on the use of this attribute.
543      *
544      * @return the value of the attribute {@code size} or an empty string if that attribute isn't defined
545      */
546     public final String getSizeAttribute() {
547         return getAttributeDirect("size");
548     }
549 
550     /**
551      * Returns the size or 1 if not defined or not convertable to int.
552      *
553      * @return the size or 1 if not defined or not convertable to int
554      */
555     public final int getSize() {
556         int size = 0;
557         final String sizeAttribute = getSizeAttribute();
558         if (ATTRIBUTE_NOT_DEFINED != sizeAttribute && ATTRIBUTE_VALUE_EMPTY != sizeAttribute) {
559             try {
560                 size = Integer.parseInt(sizeAttribute);
561             }
562             catch (final NumberFormatException ignored) {
563                 // silently ignore
564             }
565         }
566         return size;
567     }
568 
569     /**
570      * Returns the value of the attribute {@code multiple}. Refer to the <a
571      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
572      *
573      * @return the value of the attribute {@code multiple} or an empty string if that attribute isn't defined
574      */
575     public final String getMultipleAttribute() {
576         return getAttributeDirect("multiple");
577     }
578 
579     /**
580      * {@inheritDoc}
581      */
582     @Override
583     public final String getDisabledAttribute() {
584         return getAttributeDirect(ATTRIBUTE_DISABLED);
585     }
586 
587     /**
588      * {@inheritDoc}
589      */
590     @Override
591     public final boolean isDisabled() {
592         if (hasAttribute(ATTRIBUTE_DISABLED)) {
593             return true;
594         }
595 
596         Node node = getParentNode();
597         while (node != null) {
598             if (node instanceof DisabledElement element
599                     && element.isDisabled()) {
600                 return true;
601             }
602             node = node.getParentNode();
603         }
604 
605         return false;
606     }
607 
608     /**
609      * Returns {@code true} if this element is read only.
610      * @return {@code true} if this element is read only
611      */
612     public boolean isReadOnly() {
613         return hasAttribute("readOnly");
614     }
615 
616     /**
617      * Returns the value of the attribute {@code tabindex}. Refer to the <a
618      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
619      *
620      * @return the value of the attribute {@code tabindex} or an empty string if that attribute isn't defined
621      */
622     public final String getTabIndexAttribute() {
623         return getAttributeDirect("tabindex");
624     }
625 
626     /**
627      * Returns the value of the attribute {@code onfocus}. Refer to the <a
628      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
629      *
630      * @return the value of the attribute {@code onfocus} or an empty string if that attribute isn't defined
631      */
632     public final String getOnFocusAttribute() {
633         return getAttributeDirect("onfocus");
634     }
635 
636     /**
637      * Returns the value of the attribute {@code onblur}. Refer to the <a
638      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
639      *
640      * @return the value of the attribute {@code onblur} or an empty string if that attribute isn't defined
641      */
642     public final String getOnBlurAttribute() {
643         return getAttributeDirect("onblur");
644     }
645 
646     /**
647      * Returns the value of the attribute {@code onchange}. Refer to the <a
648      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
649      *
650      * @return the value of the attribute {@code onchange} or an empty string if that attribute isn't defined
651      */
652     public final String getOnChangeAttribute() {
653         return getAttributeDirect("onchange");
654     }
655 
656     /**
657      * {@inheritDoc}
658      */
659     @Override
660     public DisplayStyle getDefaultStyleDisplay() {
661         return DisplayStyle.INLINE_BLOCK;
662     }
663 
664     /**
665      * Returns the value of the {@code selectedIndex} property.
666      * @return the selectedIndex property
667      */
668     public int getSelectedIndex() {
669         final List<HtmlOption> selectedOptions = getSelectedOptions();
670         if (selectedOptions.isEmpty()) {
671             return -1;
672         }
673         final List<HtmlOption> allOptions = getOptions();
674         return allOptions.indexOf(selectedOptions.get(0));
675     }
676 
677     /**
678      * Sets the value of the {@code selectedIndex} property.
679      * @param index the new value
680      */
681     public void setSelectedIndex(final int index) {
682         for (final HtmlOption itemToUnSelect : getSelectedOptions()) {
683             setSelectedAttribute(itemToUnSelect, false);
684         }
685         if (index < 0) {
686             return;
687         }
688 
689         final List<HtmlOption> allOptions = getOptions();
690 
691         if (index < allOptions.size()) {
692             final HtmlOption itemToSelect = allOptions.get(index);
693             setSelectedAttribute(itemToSelect, true, false, true, false, true);
694         }
695     }
696 
697     /**
698      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
699      *
700      * Resets the selectedIndex if needed.
701      */
702     public void ensureSelectedIndex() {
703         if (getOptionSize() == 0) {
704             setSelectedIndex(-1);
705         }
706         else if (getSelectedIndex() == -1 && !isMultipleSelectEnabled()) {
707             setSelectedIndex(0);
708         }
709     }
710 
711     /**
712      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
713      *
714      * @param option the option to search for
715      * @return the index of the provided option or zero if not found
716      */
717     public int indexOf(final HtmlOption option) {
718         if (option == null) {
719             return 0;
720         }
721 
722         int index = 0;
723         for (final HtmlElement element : getHtmlElementDescendants()) {
724             if (element instanceof HtmlOption) {
725                 if (option == element) {
726                     return index;
727                 }
728                 index++;
729             }
730         }
731         return 0;
732     }
733 
734     /**
735      * {@inheritDoc}
736      */
737     @Override
738     protected boolean isRequiredSupported() {
739         return true;
740     }
741 
742     /**
743      * {@inheritDoc}
744      */
745     @Override
746     public boolean willValidate() {
747         return !isDisabled();
748     }
749 
750     /**
751      * {@inheritDoc}
752      */
753     @Override
754     public void setCustomValidity(final String message) {
755         customValidity_ = message;
756     }
757 
758     /**
759      * {@inheritDoc}
760      */
761     @Override
762     public boolean isValid() {
763         return isValidValidityState();
764     }
765 
766     /**
767      * {@inheritDoc}
768      */
769     @Override
770     public boolean isCustomErrorValidityState() {
771         return !StringUtils.isEmptyOrNull(customValidity_);
772     }
773 
774     @Override
775     public boolean isValidValidityState() {
776         return !isCustomErrorValidityState()
777                 && !isValueMissingValidityState();
778     }
779 
780     /**
781      * {@inheritDoc}
782      */
783     @Override
784     public boolean isValueMissingValidityState() {
785         return ATTRIBUTE_NOT_DEFINED != getAttributeDirect(ATTRIBUTE_REQUIRED)
786                 && getSelectedOptions().isEmpty();
787     }
788 }