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                     ensureSelectedIndex();
225                     return;
226                 }
227                 i++;
228             }
229         }
230 
231         if (newOption.isSelected()) {
232             setSelectedAttribute(newOption, true);
233         }
234     }
235 
236     /**
237      * Add a new option at the end.
238      * @param newOption the new option to add
239      */
240     public void appendOption(final HtmlOption newOption) {
241         appendChild(newOption);
242 
243         ensureSelectedIndex();
244     }
245 
246     /**
247      * {@inheritDoc}
248      */
249     @Override
250     public DomNode appendChild(final Node node) {
251         final DomNode response = super.appendChild(node);
252         if (node instanceof HtmlOption option) {
253             if (option.isSelected()) {
254                 doSelectOption(option, true, false, false, false);
255             }
256         }
257         return response;
258     }
259 
260     /**
261      * Sets the "selected" state of the specified option. If this "select" element
262      * is single-select, then calling this method will deselect all other options.
263      * <p>
264      * Only options that are actually in the document may be selected.
265      *
266      * @param isSelected true if the option is to become selected
267      * @param optionValue the value of the option that is to change
268      * @param <P> the page type
269      * @return the page contained in the current window as returned
270      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
271      */
272     public <P extends Page> P setSelectedAttribute(final String optionValue, final boolean isSelected) {
273         return setSelectedAttribute(optionValue, isSelected, true);
274     }
275 
276     /**
277      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
278      *
279      * Sets the "selected" state of the specified option. If this "select" element
280      * is single-select, then calling this method will deselect all other options.
281      * <p>
282      * Only options that are actually in the document may be selected.
283      *
284      * @param isSelected true if the option is to become selected
285      * @param optionValue the value of the option that is to change
286      * @param invokeOnFocus whether to set focus or not.
287      * @param <P> the page type
288      * @return the page contained in the current window as returned
289      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
290      */
291     @SuppressWarnings("unchecked")
292     public <P extends Page> P setSelectedAttribute(final String optionValue,
293             final boolean isSelected, final boolean invokeOnFocus) {
294         try {
295             final HtmlOption selected = getOptionByValue(optionValue);
296             return setSelectedAttribute(selected, isSelected, invokeOnFocus, true, false, true);
297         }
298         catch (final ElementNotFoundException e) {
299             for (final HtmlOption o : getSelectedOptions()) {
300                 o.setSelected(false);
301             }
302             return (P) getPage();
303         }
304     }
305 
306     /**
307      * Sets the "selected" state of the specified option. If this "select" element
308      * is single-select, then calling this method will deselect all other options.
309      * <p>
310      * Only options that are actually in the document may be selected.
311      *
312      * @param isSelected true if the option is to become selected
313      * @param selectedOption the value of the option that is to change
314      * @param <P> the page type
315      * @return the page contained in the current window as returned
316      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
317      */
318     public <P extends Page> P setSelectedAttribute(final HtmlOption selectedOption, final boolean isSelected) {
319         return setSelectedAttribute(selectedOption, isSelected, true, true, false, true);
320     }
321 
322     /**
323      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
324      *
325      * Sets the "selected" state of the specified option. If this "select" element
326      * is single-select, then calling this method will deselect all other options.
327      * <p>
328      * Only options that are actually in the document may be selected.
329      *
330      * @param isSelected true if the option is to become selected
331      * @param selectedOption the value of the option that is to change
332      * @param invokeOnFocus whether to set focus or not.
333      * @param shiftKey {@code true} if SHIFT is pressed
334      * @param ctrlKey {@code true} if CTRL is pressed
335      * @param isClick is mouse clicked
336      * @param <P> the page type
337      * @return the page contained in the current window as returned
338      *         by {@link org.htmlunit.WebClient#getCurrentWindow()}
339      */
340     @SuppressWarnings("unchecked")
341     public <P extends Page> P setSelectedAttribute(final HtmlOption selectedOption, final boolean isSelected,
342         final boolean invokeOnFocus, final boolean shiftKey, final boolean ctrlKey, final boolean isClick) {
343         if (isSelected && invokeOnFocus) {
344             ((HtmlPage) getPage()).setFocusedElement(this);
345         }
346 
347         final boolean changeSelectedState = selectedOption.isSelected() != isSelected;
348 
349         if (changeSelectedState) {
350             doSelectOption(selectedOption, isSelected, shiftKey, ctrlKey, isClick);
351             HtmlInput.executeOnChangeHandlerIfAppropriate(this);
352         }
353 
354         return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
355     }
356 
357     private void doSelectOption(final HtmlOption selectedOption,
358             final boolean isSelected, final boolean shiftKey, final boolean ctrlKey, final boolean isClick) {
359         // caution the HtmlOption may have been created from js and therefore the select now need
360         // to "know" that it is selected
361         if (isMultipleSelectEnabled()) {
362             selectedOption.setSelectedInternal(isSelected);
363             if (isClick && !ctrlKey) {
364                 if (!shiftKey) {
365                     setOnlySelected(selectedOption, isSelected);
366                     lastSelectedIndex_ = getOptions().indexOf(selectedOption);
367                 }
368                 else if (isSelected && lastSelectedIndex_ != -1) {
369                     final List<HtmlOption> options = getOptions();
370                     final int newIndex = options.indexOf(selectedOption);
371                     for (int i = 0; i < options.size(); i++) {
372                         options.get(i).setSelectedInternal(isBetween(i, lastSelectedIndex_, newIndex));
373                     }
374                 }
375             }
376         }
377         else {
378             setOnlySelected(selectedOption, isSelected);
379         }
380     }
381 
382     /**
383      * Sets the given {@link HtmlOption} as the only selected one.
384      * @param selectedOption the selected {@link HtmlOption}
385      * @param isSelected whether selected or not
386      */
387     void setOnlySelected(final HtmlOption selectedOption, final boolean isSelected) {
388         for (final HtmlOption option : getOptions()) {
389             option.setSelectedInternal(option == selectedOption && isSelected);
390         }
391     }
392 
393     private static boolean isBetween(final int number, final int min, final int max) {
394         return max > min ? number >= min && number <= max : number >= max && number <= min;
395     }
396 
397     /**
398      * {@inheritDoc}
399      */
400     @Override
401     public NameValuePair[] getSubmitNameValuePairs() {
402         final String name = getNameAttribute();
403 
404         final List<HtmlOption> selectedOptions = getSelectedOptions();
405 
406         final NameValuePair[] pairs = new NameValuePair[selectedOptions.size()];
407 
408         int i = 0;
409         for (final HtmlOption option : selectedOptions) {
410             pairs[i++] = new NameValuePair(name, option.getValueAttribute());
411         }
412         return pairs;
413     }
414 
415     /**
416      * Indicates if this select is submittable
417      * @return {@code false} if not
418      */
419     boolean isValidForSubmission() {
420         return getOptionSize() > 0;
421     }
422 
423     /**
424      * Returns the value of this element to what it was at the time the page was loaded.
425      */
426     @Override
427     public void reset() {
428         for (final HtmlOption option : getOptions()) {
429             option.reset();
430         }
431         onAllChildrenAddedToPage(false);
432     }
433 
434     /**
435      * {@inheritDoc}
436      * @see SubmittableElement#setDefaultValue(String)
437      */
438     @Override
439     public void setDefaultValue(final String defaultValue) {
440         setSelectedAttribute(defaultValue, true);
441     }
442 
443     /**
444      * {@inheritDoc}
445      * @see SubmittableElement#setDefaultValue(String)
446      */
447     @Override
448     public String getDefaultValue() {
449         final List<HtmlOption> options = getSelectedOptions();
450         if (options.isEmpty()) {
451             return "";
452         }
453         return options.get(0).getValueAttribute();
454     }
455 
456     /**
457      * {@inheritDoc}
458      * This implementation is empty; only checkboxes and radio buttons
459      * really care what the default checked value is.
460      * @see SubmittableElement#setDefaultChecked(boolean)
461      * @see HtmlRadioButtonInput#setDefaultChecked(boolean)
462      * @see HtmlCheckBoxInput#setDefaultChecked(boolean)
463      */
464     @Override
465     public void setDefaultChecked(final boolean defaultChecked) {
466         // Empty.
467     }
468 
469     /**
470      * {@inheritDoc}
471      * This implementation returns {@code false}; only checkboxes and
472      * radio buttons really care what the default checked value is.
473      * @see SubmittableElement#isDefaultChecked()
474      * @see HtmlRadioButtonInput#isDefaultChecked()
475      * @see HtmlCheckBoxInput#isDefaultChecked()
476      */
477     @Override
478     public boolean isDefaultChecked() {
479         return false;
480     }
481 
482     /**
483      * Returns {@code true} if this select is using "multiple select".
484      * @return {@code true} if this select is using "multiple select"
485      */
486     public boolean isMultipleSelectEnabled() {
487         return getAttributeDirect("multiple") != ATTRIBUTE_NOT_DEFINED;
488     }
489 
490     /**
491      * Returns the {@link HtmlOption} object that corresponds to the specified value.
492      *
493      * @param value the value to search by
494      * @return the {@link HtmlOption} object that corresponds to the specified value
495      * @exception ElementNotFoundException If a particular element could not be found in the DOM model
496      */
497     public HtmlOption getOptionByValue(final String value) throws ElementNotFoundException {
498         WebAssert.notNull(VALUE_ATTRIBUTE, value);
499         for (final HtmlOption option : getOptions()) {
500             if (option.getValueAttribute().equals(value)) {
501                 return option;
502             }
503         }
504         throw new ElementNotFoundException("option", VALUE_ATTRIBUTE, value);
505     }
506 
507     /**
508      * Returns the {@link HtmlOption} object that has the specified text.
509      *
510      * @param text the text to search by
511      * @return the {@link HtmlOption} object that has the specified text
512      * @exception ElementNotFoundException If a particular element could not be found in the DOM model
513      */
514     public HtmlOption getOptionByText(final String text) throws ElementNotFoundException {
515         WebAssert.notNull("text", text);
516         for (final HtmlOption option : getOptions()) {
517             if (option.getText().equals(text)) {
518                 return option;
519             }
520         }
521         throw new ElementNotFoundException("option", "text", text);
522     }
523 
524     /**
525      * Returns the value of the attribute {@code name}. Refer to the <a
526      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
527      *
528      * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
529      */
530     public final String getNameAttribute() {
531         return getAttributeDirect(NAME_ATTRIBUTE);
532     }
533 
534     /**
535      * Returns the value of the attribute {@code size}. Refer to the <a
536      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for
537      * details on the use of this attribute.
538      *
539      * @return the value of the attribute {@code size} or an empty string if that attribute isn't defined
540      */
541     public final String getSizeAttribute() {
542         return getAttributeDirect("size");
543     }
544 
545     /**
546      * @return the size or 1 if not defined or not convertable to int
547      */
548     public final int getSize() {
549         int size = 0;
550         final String sizeAttribute = getSizeAttribute();
551         if (ATTRIBUTE_NOT_DEFINED != sizeAttribute && ATTRIBUTE_VALUE_EMPTY != sizeAttribute) {
552             try {
553                 size = Integer.parseInt(sizeAttribute);
554             }
555             catch (final Exception ignored) {
556                 // silently ignore
557             }
558         }
559         return size;
560     }
561 
562     /**
563      * Returns the value of the attribute {@code multiple}. Refer to the <a
564      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
565      *
566      * @return the value of the attribute {@code multiple} or an empty string if that attribute isn't defined
567      */
568     public final String getMultipleAttribute() {
569         return getAttributeDirect("multiple");
570     }
571 
572     /**
573      * {@inheritDoc}
574      */
575     @Override
576     public final String getDisabledAttribute() {
577         return getAttributeDirect(ATTRIBUTE_DISABLED);
578     }
579 
580     /**
581      * {@inheritDoc}
582      */
583     @Override
584     public final boolean isDisabled() {
585         if (hasAttribute(ATTRIBUTE_DISABLED)) {
586             return true;
587         }
588 
589         Node node = getParentNode();
590         while (node != null) {
591             if (node instanceof DisabledElement element
592                     && element.isDisabled()) {
593                 return true;
594             }
595             node = node.getParentNode();
596         }
597 
598         return false;
599     }
600 
601     /**
602      * Returns {@code true} if this element is read only.
603      * @return {@code true} if this element is read only
604      */
605     public boolean isReadOnly() {
606         return hasAttribute("readOnly");
607     }
608 
609     /**
610      * Returns the value of the attribute {@code tabindex}. Refer to the <a
611      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
612      *
613      * @return the value of the attribute {@code tabindex} or an empty string if that attribute isn't defined
614      */
615     public final String getTabIndexAttribute() {
616         return getAttributeDirect("tabindex");
617     }
618 
619     /**
620      * Returns the value of the attribute {@code onfocus}. Refer to the <a
621      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
622      *
623      * @return the value of the attribute {@code onfocus} or an empty string if that attribute isn't defined
624      */
625     public final String getOnFocusAttribute() {
626         return getAttributeDirect("onfocus");
627     }
628 
629     /**
630      * Returns the value of the attribute {@code onblur}. Refer to the <a
631      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
632      *
633      * @return the value of the attribute {@code onblur} or an empty string if that attribute isn't defined
634      */
635     public final String getOnBlurAttribute() {
636         return getAttributeDirect("onblur");
637     }
638 
639     /**
640      * Returns the value of the attribute {@code onchange}. Refer to the <a
641      * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
642      *
643      * @return the value of the attribute {@code onchange} or an empty string if that attribute isn't defined
644      */
645     public final String getOnChangeAttribute() {
646         return getAttributeDirect("onchange");
647     }
648 
649     /**
650      * {@inheritDoc}
651      */
652     @Override
653     public DisplayStyle getDefaultStyleDisplay() {
654         return DisplayStyle.INLINE_BLOCK;
655     }
656 
657     /**
658      * Returns the value of the {@code selectedIndex} property.
659      * @return the selectedIndex property
660      */
661     public int getSelectedIndex() {
662         final List<HtmlOption> selectedOptions = getSelectedOptions();
663         if (selectedOptions.isEmpty()) {
664             return -1;
665         }
666         final List<HtmlOption> allOptions = getOptions();
667         return allOptions.indexOf(selectedOptions.get(0));
668     }
669 
670     /**
671      * Sets the value of the {@code selectedIndex} property.
672      * @param index the new value
673      */
674     public void setSelectedIndex(final int index) {
675         for (final HtmlOption itemToUnSelect : getSelectedOptions()) {
676             setSelectedAttribute(itemToUnSelect, false);
677         }
678         if (index < 0) {
679             return;
680         }
681 
682         final List<HtmlOption> allOptions = getOptions();
683 
684         if (index < allOptions.size()) {
685             final HtmlOption itemToSelect = allOptions.get(index);
686             setSelectedAttribute(itemToSelect, true, false, true, false, true);
687         }
688     }
689 
690     /**
691      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
692      *
693      * Resets the selectedIndex if needed.
694      */
695     public void ensureSelectedIndex() {
696         if (getOptionSize() == 0) {
697             setSelectedIndex(-1);
698         }
699         else if (getSelectedIndex() == -1 && !isMultipleSelectEnabled()) {
700             setSelectedIndex(0);
701         }
702     }
703 
704     /**
705      * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
706      *
707      * @param option the option to search for
708      * @return the index of the provided option or zero if not found
709      */
710     public int indexOf(final HtmlOption option) {
711         if (option == null) {
712             return 0;
713         }
714 
715         int index = 0;
716         for (final HtmlElement element : getHtmlElementDescendants()) {
717             if (option == element) {
718                 return index;
719             }
720             index++;
721         }
722         return 0;
723     }
724 
725     /**
726      * {@inheritDoc}
727      */
728     @Override
729     protected boolean isRequiredSupported() {
730         return true;
731     }
732 
733     /**
734      * {@inheritDoc}
735      */
736     @Override
737     public boolean willValidate() {
738         return !isDisabled();
739     }
740 
741     /**
742      * {@inheritDoc}
743      */
744     @Override
745     public void setCustomValidity(final String message) {
746         customValidity_ = message;
747     }
748 
749     /**
750      * {@inheritDoc}
751      */
752     @Override
753     public boolean isValid() {
754         return isValidValidityState();
755     }
756 
757     /**
758      * {@inheritDoc}
759      */
760     @Override
761     public boolean isCustomErrorValidityState() {
762         return !StringUtils.isEmptyOrNull(customValidity_);
763     }
764 
765     @Override
766     public boolean isValidValidityState() {
767         return !isCustomErrorValidityState()
768                 && !isValueMissingValidityState();
769     }
770 
771     /**
772      * {@inheritDoc}
773      */
774     @Override
775     public boolean isValueMissingValidityState() {
776         return ATTRIBUTE_NOT_DEFINED != getAttributeDirect(ATTRIBUTE_REQUIRED)
777                 && getSelectedOptions().isEmpty();
778     }
779 }