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