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.Iterator;
20 import java.util.List;
21 import java.util.Map;
22
23 import org.htmlunit.ElementNotFoundException;
24 import org.htmlunit.Page;
25 import org.htmlunit.SgmlPage;
26 import org.htmlunit.WebAssert;
27 import org.htmlunit.javascript.host.event.Event;
28 import org.htmlunit.javascript.host.event.MouseEvent;
29 import org.htmlunit.util.NameValuePair;
30 import org.htmlunit.util.StringUtils;
31 import org.w3c.dom.Node;
32
33 /**
34 * Wrapper for the HTML element "select".
35 *
36 * @author Mike Bowler
37 * @author Mike J. Bresnahan
38 * @author David K. Taylor
39 * @author Christian Sell
40 * @author David D. Kilzer
41 * @author Marc Guillemot
42 * @author Daniel Gredler
43 * @author Ahmed Ashour
44 * @author Ronald Brill
45 * @author Frank Danek
46 * @author Lai Quang Duong
47 */
48 public class HtmlSelect extends HtmlElement implements DisabledElement, SubmittableElement,
49 LabelableElement, ValidatableHtmlElement {
50
51 /** The HTML tag represented by this element. */
52 public static final String TAG_NAME = "select";
53
54 /** The index of the HtmlOption which was last selected. */
55 private int lastSelectedIndex_ = -1;
56 private String customValidity_;
57
58 /**
59 * Creates an instance.
60 *
61 * @param qualifiedName the qualified name of the element type to instantiate
62 * @param page the page that contains this element
63 * @param attributes the initial attributes
64 */
65 HtmlSelect(final String qualifiedName, final SgmlPage page,
66 final Map<String, DomAttr> attributes) {
67 super(qualifiedName, page, attributes);
68 }
69
70 /**
71 * If we were given an invalid <code>size</code> attribute, normalize it.
72 * Then set a default selected option if none was specified and the size is 1 or less
73 * and this isn't a multiple selection input.
74 * @param postponed unused in this implementation; kept to satisfy the overridden signature,
75 * see {@link org.htmlunit.javascript.PostponedAction}
76 */
77 @Override
78 public void onAllChildrenAddedToPage(final boolean postponed) {
79 // Fix the size if necessary.
80 int size;
81 try {
82 size = Integer.parseInt(getSizeAttribute());
83 if (size < 0) {
84 size = 0;
85 }
86 }
87 catch (final NumberFormatException e) {
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, an empty list 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. This is a no-op (no exception thrown)
193 * if no option exists at the given index.
194 * @param index the index of the option to remove
195 */
196 public void removeOption(final int index) {
197 final Iterator<HtmlElement> iterator = getHtmlElementDescendants().iterator();
198 int i = 0;
199 while (iterator.hasNext()) {
200 final DomElement element = iterator.next();
201 if (element instanceof HtmlOption) {
202 if (i == index) {
203 element.remove();
204 ensureSelectedIndex();
205 return;
206 }
207 i++;
208 }
209 }
210 }
211
212 /**
213 * Replace an option at the given index with a new option. This is a no-op
214 * (no exception thrown) if no option exists at the given index.
215 * @param index the index of the option to remove
216 * @param newOption the new option to replace to indexed option
217 */
218 public void replaceOption(final int index, final HtmlOption newOption) {
219 final Iterator<HtmlElement> iterator = getHtmlElementDescendants().iterator();
220 int i = 0;
221 while (iterator.hasNext()) {
222 final DomElement element = iterator.next();
223 if (element instanceof HtmlOption) {
224 if (i == index) {
225 element.replace(newOption);
226
227 if (newOption.isSelected() && !isMultipleSelectEnabled()) {
228 setOnlySelected(newOption, true);
229 }
230 ensureSelectedIndex();
231
232 return;
233 }
234 i++;
235 }
236 }
237 }
238
239 /**
240 * Add a new option at the end.
241 * @param newOption the new option to add
242 */
243 public void appendOption(final HtmlOption newOption) {
244 appendChild(newOption);
245
246 ensureSelectedIndex();
247 }
248
249 /**
250 * {@inheritDoc}
251 * If the appended node is a selected {@link HtmlOption}, this select's
252 * selection state (and any related deselection of sibling options) is
253 * updated accordingly.
254 */
255 @Override
256 public DomNode appendChild(final Node node) {
257 final DomNode response = super.appendChild(node);
258 if (node instanceof HtmlOption option) {
259 if (option.isSelected()) {
260 doSelectOption(option, true, false, false, false);
261 }
262 }
263 return response;
264 }
265
266 /**
267 * Sets the "selected" state of the specified option. If this "select" element
268 * is single-select, then calling this method will deselect all other options.
269 * <p>
270 * Only options that are actually in the document may be selected.
271 * </p>
272 *
273 * @param <P> the page type
274 * @param optionValue the value of the option that is to change
275 * @param isSelected true if the option is to become selected
276 * @return the page contained in the current window as returned
277 * by {@link org.htmlunit.WebClient#getCurrentWindow()}
278 */
279 public <P extends Page> P setSelectedAttribute(final String optionValue, final boolean isSelected) {
280 return setSelectedAttribute(optionValue, isSelected, true);
281 }
282
283 /**
284 * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
285 *
286 * Sets the "selected" state of the specified option. If this "select" element
287 * is single-select, then calling this method will deselect all other options.
288 * <p>
289 * Only options that are actually in the document may be selected.
290 * </p>
291 *
292 * @param <P> the page type
293 * @param optionValue the value of the option that is to change
294 * @param isSelected true if the option is to become selected
295 * @param invokeOnFocus whether to set focus or not.
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 * </p>
320 *
321 * @param <P> the page type
322 * @param selectedOption the option that is to change
323 * @param isSelected true if the option is to become selected
324 * @return the page contained in the current window as returned
325 * by {@link org.htmlunit.WebClient#getCurrentWindow()}
326 */
327 public <P extends Page> P setSelectedAttribute(final HtmlOption selectedOption, final boolean isSelected) {
328 return setSelectedAttribute(selectedOption, isSelected, true, true, false, true);
329 }
330
331 /**
332 * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
333 *
334 * Sets the "selected" state of the specified option. If this "select" element
335 * is single-select, then calling this method will deselect all other options.
336 * <p>
337 * Only options that are actually in the document may be selected.
338 * </p>
339 *
340 * @param <P> the page type
341 * @param selectedOption the option that is to change
342 * @param isSelected true if the option is to become selected
343 * @param invokeOnFocus whether to set focus or not.
344 * @param shiftKey {@code true} if SHIFT is pressed
345 * @param ctrlKey {@code true} if CTRL is pressed
346 * @param isClick is mouse clicked
347 * @return the page contained in the current window as returned
348 * by {@link org.htmlunit.WebClient#getCurrentWindow()}
349 */
350 @SuppressWarnings("unchecked")
351 public <P extends Page> P setSelectedAttribute(final HtmlOption selectedOption, final boolean isSelected,
352 final boolean invokeOnFocus, final boolean shiftKey, final boolean ctrlKey, final boolean isClick) {
353 if (isSelected && invokeOnFocus) {
354 ((HtmlPage) getPage()).setFocusedElement(this);
355 }
356
357 final boolean changeSelectedState = selectedOption.isSelected() != isSelected;
358
359 if (changeSelectedState) {
360 doSelectOption(selectedOption, isSelected, shiftKey, ctrlKey, isClick);
361 HtmlInput.executeOnChangeHandlerIfAppropriate(this);
362 }
363
364 return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
365 }
366
367 private void doSelectOption(final HtmlOption selectedOption,
368 final boolean isSelected, final boolean shiftKey, final boolean ctrlKey, final boolean isClick) {
369 // caution the HtmlOption may have been created from js and therefore the select now need
370 // to "know" that it is selected
371 if (isMultipleSelectEnabled()) {
372 selectedOption.setSelectedInternal(isSelected);
373 if (isClick && !ctrlKey) {
374 if (!shiftKey) {
375 setOnlySelected(selectedOption, isSelected);
376 lastSelectedIndex_ = getOptions().indexOf(selectedOption);
377 }
378 else if (isSelected && lastSelectedIndex_ != -1) {
379 final List<HtmlOption> options = getOptions();
380 final int newIndex = options.indexOf(selectedOption);
381 for (int i = 0; i < options.size(); i++) {
382 options.get(i).setSelectedInternal(isBetween(i, lastSelectedIndex_, newIndex));
383 }
384 }
385 }
386 }
387 else {
388 setOnlySelected(selectedOption, isSelected);
389 }
390 }
391
392 /**
393 * Sets the given {@link HtmlOption} as the only selected one.
394 * @param selectedOption the selected {@link HtmlOption}
395 * @param isSelected whether selected or not
396 */
397 void setOnlySelected(final HtmlOption selectedOption, final boolean isSelected) {
398 for (final HtmlOption option : getOptions()) {
399 option.setSelectedInternal(option == selectedOption && isSelected);
400 }
401 }
402
403 private static boolean isBetween(final int number, final int min, final int max) {
404 return max > min ? number >= min && number <= max : number >= max && number <= min;
405 }
406
407 /**
408 * {@inheritDoc}
409 */
410 @Override
411 public NameValuePair[] getSubmitNameValuePairs() {
412 final String name = getNameAttribute();
413
414 final List<HtmlOption> selectedOptions = getSelectedOptions();
415
416 final NameValuePair[] pairs = new NameValuePair[selectedOptions.size()];
417
418 int i = 0;
419 for (final HtmlOption option : selectedOptions) {
420 pairs[i++] = new NameValuePair(name, option.getValueAttribute());
421 }
422 return pairs;
423 }
424
425 /**
426 * Indicates if this select is submittable.
427 * @return {@code false} if not
428 */
429 boolean isValidForSubmission() {
430 return getOptionSize() > 0;
431 }
432
433 /**
434 * Returns the value of this element to what it was at the time the page was loaded.
435 */
436 @Override
437 public void reset() {
438 for (final HtmlOption option : getOptions()) {
439 option.reset();
440 }
441 onAllChildrenAddedToPage(false);
442 }
443
444 /**
445 * {@inheritDoc}
446 * @see SubmittableElement#setDefaultValue(String)
447 */
448 @Override
449 public void setDefaultValue(final String defaultValue) {
450 setSelectedAttribute(defaultValue, true);
451 }
452
453 /**
454 * {@inheritDoc}
455 * @see SubmittableElement#setDefaultValue(String)
456 */
457 @Override
458 public String getDefaultValue() {
459 final List<HtmlOption> options = getSelectedOptions();
460 if (options.isEmpty()) {
461 return "";
462 }
463 return options.get(0).getValueAttribute();
464 }
465
466 /**
467 * {@inheritDoc}
468 * This implementation is empty; only checkboxes and radio buttons
469 * really care what the default checked value is.
470 * @see SubmittableElement#setDefaultChecked(boolean)
471 * @see HtmlRadioButtonInput#setDefaultChecked(boolean)
472 * @see HtmlCheckBoxInput#setDefaultChecked(boolean)
473 */
474 @Override
475 public void setDefaultChecked(final boolean defaultChecked) {
476 // Empty.
477 }
478
479 /**
480 * {@inheritDoc}
481 * This implementation returns {@code false}; only checkboxes and
482 * radio buttons really care what the default checked value is.
483 * @see SubmittableElement#isDefaultChecked()
484 * @see HtmlRadioButtonInput#isDefaultChecked()
485 * @see HtmlCheckBoxInput#isDefaultChecked()
486 */
487 @Override
488 public boolean isDefaultChecked() {
489 return false;
490 }
491
492 /**
493 * Returns {@code true} if this select is using "multiple select".
494 * @return {@code true} if this select is using "multiple select"
495 */
496 public boolean isMultipleSelectEnabled() {
497 return getAttributeDirect("multiple") != ATTRIBUTE_NOT_DEFINED;
498 }
499
500 /**
501 * Returns the {@link HtmlOption} object that corresponds to the specified value.
502 *
503 * @param value the value to search by
504 * @return the {@link HtmlOption} object that corresponds to the specified value
505 * @throws ElementNotFoundException If a particular element could not be found in the DOM model
506 */
507 public HtmlOption getOptionByValue(final String value) throws ElementNotFoundException {
508 WebAssert.notNull(VALUE_ATTRIBUTE, value);
509 for (final HtmlOption option : getOptions()) {
510 if (option.getValueAttribute().equals(value)) {
511 return option;
512 }
513 }
514 throw new ElementNotFoundException("option", VALUE_ATTRIBUTE, value);
515 }
516
517 /**
518 * Returns the {@link HtmlOption} object that has the specified text.
519 *
520 * @param text the text to search by
521 * @return the {@link HtmlOption} object that has the specified text
522 * @throws ElementNotFoundException If a particular element could not be found in the DOM model
523 */
524 public HtmlOption getOptionByText(final String text) throws ElementNotFoundException {
525 WebAssert.notNull("text", text);
526 for (final HtmlOption option : getOptions()) {
527 if (option.getText().equals(text)) {
528 return option;
529 }
530 }
531 throw new ElementNotFoundException("option", "text", text);
532 }
533
534 /**
535 * Returns the value of the attribute {@code name}. Refer to the <a
536 * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
537 *
538 * @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
539 */
540 public final String getNameAttribute() {
541 return getAttributeDirect(NAME_ATTRIBUTE);
542 }
543
544 /**
545 * Returns the value of the attribute {@code size}. Refer to the <a
546 * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for
547 * details on the use of this attribute.
548 *
549 * @return the value of the attribute {@code size} or an empty string if that attribute isn't defined
550 */
551 public final String getSizeAttribute() {
552 return getAttributeDirect("size");
553 }
554
555 /**
556 * Returns the size or 0 if not defined or not convertable to int.
557 * NOTE: this currently diverges from the HTML specification, which defines
558 * the default {@code size} IDL value as 1 for a single select and 4 for a
559 * multiple select.
560 *
561 * @return the size or 0 if not defined or not convertable to int
562 */
563 public final int getSize() {
564 final String sizeAttribute = getSizeAttribute();
565 if (ATTRIBUTE_NOT_DEFINED != sizeAttribute && ATTRIBUTE_VALUE_EMPTY != sizeAttribute) {
566 try {
567 final int size = Integer.parseInt(getSizeAttribute());
568 if (size < 0) {
569 return 0;
570 }
571 return size;
572 }
573 catch (final NumberFormatException ignored) {
574 // silently ignore
575 }
576 }
577 return 0;
578 }
579
580 /**
581 * Returns the value of the attribute {@code multiple}. Refer to the <a
582 * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
583 *
584 * @return the value of the attribute {@code multiple} or an empty string if that attribute isn't defined
585 */
586 public final String getMultipleAttribute() {
587 return getAttributeDirect("multiple");
588 }
589
590 /**
591 * {@inheritDoc}
592 */
593 @Override
594 public final String getDisabledAttribute() {
595 return getAttributeDirect(ATTRIBUTE_DISABLED);
596 }
597
598 /**
599 * Returns the value of the attribute {@code tabindex}. Refer to the <a
600 * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
601 *
602 * @return the value of the attribute {@code tabindex} or an empty string if that attribute isn't defined
603 */
604 public final String getTabIndexAttribute() {
605 return getAttributeDirect("tabindex");
606 }
607
608 /**
609 * Returns the value of the attribute {@code onfocus}. Refer to the <a
610 * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
611 *
612 * @return the value of the attribute {@code onfocus} or an empty string if that attribute isn't defined
613 */
614 public final String getOnFocusAttribute() {
615 return getAttributeDirect("onfocus");
616 }
617
618 /**
619 * Returns the value of the attribute {@code onblur}. Refer to the <a
620 * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
621 *
622 * @return the value of the attribute {@code onblur} or an empty string if that attribute isn't defined
623 */
624 public final String getOnBlurAttribute() {
625 return getAttributeDirect("onblur");
626 }
627
628 /**
629 * Returns the value of the attribute {@code onchange}. Refer to the <a
630 * href="http://www.w3.org/TR/html401/">HTML 4.01</a> documentation for details on the use of this attribute.
631 *
632 * @return the value of the attribute {@code onchange} or an empty string if that attribute isn't defined
633 */
634 public final String getOnChangeAttribute() {
635 return getAttributeDirect("onchange");
636 }
637
638 /**
639 * {@inheritDoc}
640 */
641 @Override
642 public DisplayStyle getDefaultStyleDisplay() {
643 return DisplayStyle.INLINE_BLOCK;
644 }
645
646 /**
647 * Returns the value of the {@code selectedIndex} property.
648 * @return the selectedIndex property
649 */
650 public int getSelectedIndex() {
651 final List<HtmlOption> selectedOptions = getSelectedOptions();
652 if (selectedOptions.isEmpty()) {
653 return -1;
654 }
655 final List<HtmlOption> allOptions = getOptions();
656 return allOptions.indexOf(selectedOptions.get(0));
657 }
658
659 /**
660 * Sets the value of the {@code selectedIndex} property. Any previously selected
661 * options are always deselected first; if {@code index} is negative or is out of
662 * range for the current number of options, the result is that nothing is selected.
663 * @param index the new value
664 */
665 public void setSelectedIndex(final int index) {
666 for (final HtmlOption itemToUnSelect : getSelectedOptions()) {
667 setSelectedAttribute(itemToUnSelect, false);
668 }
669 if (index < 0) {
670 return;
671 }
672
673 final List<HtmlOption> allOptions = getOptions();
674
675 if (index < allOptions.size()) {
676 final HtmlOption itemToSelect = allOptions.get(index);
677 setSelectedAttribute(itemToSelect, true, false, true, false, true);
678 }
679 }
680
681 /**
682 * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
683 *
684 * Resets the selectedIndex if needed.
685 */
686 public void ensureSelectedIndex() {
687 if (getOptionSize() == 0) {
688 setSelectedIndex(-1);
689 }
690 else if (getSelectedIndex() == -1 && !isMultipleSelectEnabled()) {
691 setSelectedIndex(0);
692 }
693 }
694
695 /**
696 * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
697 *
698 * @param option the option to search for
699 * @return the index of the provided option or zero if not found. CAUTION: this is
700 * ambiguous with an option that is genuinely found at index zero; callers cannot
701 * distinguish "not found" from "found as the first option" from the return value alone
702 */
703 public int indexOf(final HtmlOption option) {
704 if (option == null) {
705 return 0;
706 }
707
708 int index = 0;
709 for (final HtmlElement element : getHtmlElementDescendants()) {
710 if (element instanceof HtmlOption) {
711 if (option == element) {
712 return index;
713 }
714 index++;
715 }
716 }
717 return 0;
718 }
719
720 /**
721 * {@inheritDoc}
722 */
723 @Override
724 protected boolean isRequiredSupported() {
725 return true;
726 }
727
728 /**
729 * {@inheritDoc}
730 */
731 @Override
732 public boolean willValidate() {
733 return !isDisabled();
734 }
735
736 /**
737 * {@inheritDoc}
738 */
739 @Override
740 public String getCustomValidity() {
741 return customValidity_;
742 }
743
744 /**
745 * {@inheritDoc}
746 */
747 @Override
748 public void setCustomValidity(final String message) {
749 customValidity_ = message;
750 }
751
752 /**
753 * {@inheritDoc}
754 */
755 @Override
756 public boolean isValid() {
757 return isValidValidityState();
758 }
759
760 /**
761 * {@inheritDoc}
762 */
763 @Override
764 public boolean isCustomErrorValidityState() {
765 return !StringUtils.isEmptyOrNull(customValidity_);
766 }
767
768 /**
769 * {@inheritDoc}
770 */
771 @Override
772 public boolean isValidValidityState() {
773 return !isCustomErrorValidityState()
774 && !isValueMissingValidityState();
775 }
776
777 /**
778 * {@inheritDoc}
779 */
780 @Override
781 public boolean isValueMissingValidityState() {
782 if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect(ATTRIBUTE_REQUIRED)) {
783 return false;
784 }
785
786 final List<HtmlOption> selected = getSelectedOptions();
787 if (selected.isEmpty()) {
788 return true;
789 }
790
791 // per spec, this only applies to single-selection selects; a multi-select
792 // or size>1 select with at least one option selected is never "missing"
793 if (!isMultipleSelectEnabled() && getSize() <= 1) {
794 final List<HtmlOption> options = getOptions();
795 return !options.isEmpty()
796 && selected.size() == 1
797 && selected.get(0) == options.get(0)
798 && selected.get(0).getValueAttribute().isEmpty();
799 }
800
801 return false;
802 }
803
804 /**
805 * {@inheritDoc}
806 */
807 @Override
808 public String getValueMissingMessage() {
809 return "Please select an item in the list.";
810 }
811 }