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.htmlunit.ElementNotFoundException;
27 import org.htmlunit.Page;
28 import org.htmlunit.SgmlPage;
29 import org.htmlunit.WebAssert;
30 import org.htmlunit.javascript.host.event.Event;
31 import org.htmlunit.javascript.host.event.MouseEvent;
32 import org.htmlunit.util.NameValuePair;
33 import org.htmlunit.util.StringUtils;
34 import org.w3c.dom.Node;
35
36 /**
37 * Wrapper for the HTML element "select".
38 *
39 * @author Mike Bowler
40 * @author Mike J. Bresnahan
41 * @author David K. Taylor
42 * @author Christian Sell
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 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 the options in this select element.
161 * @return all 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 = 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.isEmptyOrNull(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 }