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