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