1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 package org.htmlunit.html;
16
17 import static org.htmlunit.BrowserVersionFeatures.HTMLSELECT_WILL_VALIDATE_IGNORES_READONLY;
18
19 import java.util.ArrayList;
20 import java.util.Collection;
21 import java.util.Collections;
22 import java.util.HashSet;
23 import java.util.List;
24 import java.util.Map;
25
26 import org.apache.commons.lang3.StringUtils;
27 import org.htmlunit.ElementNotFoundException;
28 import org.htmlunit.Page;
29 import org.htmlunit.SgmlPage;
30 import org.htmlunit.WebAssert;
31 import org.htmlunit.javascript.host.event.Event;
32 import org.htmlunit.javascript.host.event.MouseEvent;
33 import org.htmlunit.util.NameValuePair;
34 import org.w3c.dom.Node;
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50 public class HtmlSelect extends HtmlElement implements DisabledElement, SubmittableElement,
51 LabelableElement, FormFieldWithNameHistory, ValidatableElement {
52
53
54 public static final String TAG_NAME = "select";
55
56 private final String originalName_;
57 private Collection<String> newNames_ = Collections.emptySet();
58
59 private int lastSelectedIndex_ = -1;
60 private String customValidity_;
61
62
63
64
65
66
67
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
77
78
79
80
81 @Override
82 public void onAllChildrenAddedToPage(final boolean postponed) {
83
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
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
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
121
122
123
124
125
126
127
128
129 public List<HtmlOption> getSelectedOptions() {
130 final List<HtmlOption> result;
131 if (isMultipleSelectEnabled()) {
132
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
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
161
162
163 public List<HtmlOption> getOptions() {
164 return Collections.unmodifiableList(getStaticElementsByTagName("option"));
165 }
166
167
168
169
170
171
172
173 public HtmlOption getOption(final int index) {
174 return this.<HtmlOption>getStaticElementsByTagName("option").get(index);
175 }
176
177
178
179
180
181 public int getOptionSize() {
182 return getStaticElementsByTagName("option").size();
183 }
184
185
186
187
188
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
200
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
220
221
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
245
246
247 public void appendOption(final HtmlOption newOption) {
248 appendChild(newOption);
249
250 ensureSelectedIndex();
251 }
252
253
254
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
270
271
272
273
274
275
276
277
278
279
280 public <P extends Page> P setSelectedAttribute(final String optionValue, final boolean isSelected) {
281 return setSelectedAttribute(optionValue, isSelected, true);
282 }
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
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
316
317
318
319
320
321
322
323
324
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
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
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
368
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
392
393
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
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
425
426
427 boolean isValidForSubmission() {
428 return getOptionSize() > 0;
429 }
430
431
432
433
434 @Override
435 public void reset() {
436 for (final HtmlOption option : getOptions()) {
437 option.reset();
438 }
439 onAllChildrenAddedToPage(false);
440 }
441
442
443
444
445
446 @Override
447 public void setDefaultValue(final String defaultValue) {
448 setSelectedAttribute(defaultValue, true);
449 }
450
451
452
453
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
466
467
468
469
470
471
472 @Override
473 public void setDefaultChecked(final boolean defaultChecked) {
474
475 }
476
477
478
479
480
481
482
483
484
485 @Override
486 public boolean isDefaultChecked() {
487 return false;
488 }
489
490
491
492
493
494 public boolean isMultipleSelectEnabled() {
495 return getAttributeDirect("multiple") != ATTRIBUTE_NOT_DEFINED;
496 }
497
498
499
500
501
502
503
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
517
518
519
520
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
534
535
536
537
538 public final String getNameAttribute() {
539 return getAttributeDirect(NAME_ATTRIBUTE);
540 }
541
542
543
544
545
546
547
548
549 public final String getSizeAttribute() {
550 return getAttributeDirect("size");
551 }
552
553
554
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
565 }
566 }
567 return size;
568 }
569
570
571
572
573
574
575
576 public final String getMultipleAttribute() {
577 return getAttributeDirect("multiple");
578 }
579
580
581
582
583 @Override
584 public final String getDisabledAttribute() {
585 return getAttributeDirect(ATTRIBUTE_DISABLED);
586 }
587
588
589
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
611
612
613 public boolean isReadOnly() {
614 return hasAttribute("readOnly");
615 }
616
617
618
619
620
621
622
623 public final String getTabIndexAttribute() {
624 return getAttributeDirect("tabindex");
625 }
626
627
628
629
630
631
632
633 public final String getOnFocusAttribute() {
634 return getAttributeDirect("onfocus");
635 }
636
637
638
639
640
641
642
643 public final String getOnBlurAttribute() {
644 return getAttributeDirect("onblur");
645 }
646
647
648
649
650
651
652
653 public final String getOnChangeAttribute() {
654 return getAttributeDirect("onchange");
655 }
656
657
658
659
660 @Override
661 protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue,
662 final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) {
663 final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName);
664 if (DomElement.NAME_ATTRIBUTE.equals(qualifiedNameLC)) {
665 if (newNames_.isEmpty()) {
666 newNames_ = new HashSet<>();
667 }
668 newNames_.add(attributeValue);
669 }
670 super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners,
671 notifyMutationObservers);
672 }
673
674
675
676
677 @Override
678 public String getOriginalName() {
679 return originalName_;
680 }
681
682
683
684
685 @Override
686 public Collection<String> getNewNames() {
687 return newNames_;
688 }
689
690
691
692
693 @Override
694 public DisplayStyle getDefaultStyleDisplay() {
695 return DisplayStyle.INLINE_BLOCK;
696 }
697
698
699
700
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
713
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
733
734
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
747
748
749
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
768
769 @Override
770 protected boolean isRequiredSupported() {
771 return true;
772 }
773
774
775
776
777 @Override
778 public boolean willValidate() {
779 return !isDisabled() && (hasFeature(HTMLSELECT_WILL_VALIDATE_IGNORES_READONLY) || !isReadOnly());
780 }
781
782
783
784
785 @Override
786 public void setCustomValidity(final String message) {
787 customValidity_ = message;
788 }
789
790
791
792
793 @Override
794 public boolean isValid() {
795 return isValidValidityState();
796 }
797
798
799
800
801 @Override
802 public boolean isCustomErrorValidityState() {
803 return !StringUtils.isEmpty(customValidity_);
804 }
805
806 @Override
807 public boolean isValidValidityState() {
808 return !isCustomErrorValidityState()
809 && !isValueMissingValidityState();
810 }
811
812
813
814
815 @Override
816 public boolean isValueMissingValidityState() {
817 return ATTRIBUTE_NOT_DEFINED != getAttributeDirect(ATTRIBUTE_REQUIRED)
818 && getSelectedOptions().isEmpty();
819 }
820 }