org.htmlunit.html.HtmlSelect Maven / Gradle / Ivy
Show all versions of htmlunit Show documentation
/*
* Copyright (c) 2002-2024 Gargoyle Software Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.htmlunit.html;
import static org.htmlunit.BrowserVersionFeatures.HTMLSELECT_WILL_VALIDATE_IGNORES_READONLY;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import org.apache.commons.lang3.StringUtils;
import org.htmlunit.ElementNotFoundException;
import org.htmlunit.Page;
import org.htmlunit.SgmlPage;
import org.htmlunit.WebAssert;
import org.htmlunit.javascript.host.event.Event;
import org.htmlunit.javascript.host.event.MouseEvent;
import org.htmlunit.util.NameValuePair;
import org.w3c.dom.Node;
/**
* Wrapper for the HTML element "select".
*
* @author Mike Bowler
* @author Mike J. Bresnahan
* @author David K. Taylor
* @author Christian Sell
* @author David D. Kilzer
* @author Marc Guillemot
* @author Daniel Gredler
* @author Ahmed Ashour
* @author Ronald Brill
* @author Frank Danek
*/
public class HtmlSelect extends HtmlElement implements DisabledElement, SubmittableElement,
LabelableElement, FormFieldWithNameHistory, ValidatableElement {
/** The HTML tag represented by this element. */
public static final String TAG_NAME = "select";
private final String originalName_;
private Collection newNames_ = Collections.emptySet();
/** What is the index of the HtmlOption which was last selected. */
private int lastSelectedIndex_ = -1;
private String customValidity_;
/**
* Creates an instance.
*
* @param qualifiedName the qualified name of the element type to instantiate
* @param page the page that contains this element
* @param attributes the initial attributes
*/
HtmlSelect(final String qualifiedName, final SgmlPage page,
final Map attributes) {
super(qualifiedName, page, attributes);
originalName_ = getNameAttribute();
}
/**
* If we were given an invalid size
attribute, normalize it.
* Then set a default selected option if none was specified and the size is 1 or less
* and this isn't a multiple selection input.
* @param postponed whether to use {@link org.htmlunit.javascript.PostponedAction} or no
*/
@Override
public void onAllChildrenAddedToPage(final boolean postponed) {
// Fix the size if necessary.
int size;
try {
size = Integer.parseInt(getSizeAttribute());
if (size < 0) {
removeAttribute("size");
size = 0;
}
}
catch (final NumberFormatException e) {
removeAttribute("size");
size = 0;
}
// Set a default selected option if necessary.
if (getSelectedOptions().isEmpty() && size <= 1 && !isMultipleSelectEnabled()) {
final List options = getOptions();
if (!options.isEmpty()) {
final HtmlOption first = options.get(0);
first.setSelectedInternal(true);
}
}
}
/**
* {@inheritDoc}
*/
@Override
public boolean handles(final Event event) {
if (event instanceof MouseEvent) {
return true;
}
return super.handles(event);
}
/**
* Returns all of the currently selected options. The following special
* conditions can occur if the element is in single select mode:
*
* - if multiple options are erroneously selected, the last one is returned
* - if no options are selected, the first one is returned
*
*
* @return the currently selected options
*/
public List getSelectedOptions() {
final List result;
if (isMultipleSelectEnabled()) {
// Multiple selections possible.
result = new ArrayList<>();
for (final HtmlElement element : getHtmlElementDescendants()) {
if (element instanceof HtmlOption && ((HtmlOption) element).isSelected()) {
result.add((HtmlOption) element);
}
}
}
else {
// Only a single selection is possible.
result = new ArrayList<>(1);
HtmlOption lastSelected = null;
for (final HtmlElement element : getHtmlElementDescendants()) {
if (element instanceof HtmlOption) {
final HtmlOption option = (HtmlOption) element;
if (option.isSelected()) {
lastSelected = option;
}
}
}
if (lastSelected != null) {
result.add(lastSelected);
}
}
return Collections.unmodifiableList(result);
}
/**
* Returns all of the options in this select element.
* @return all of the options in this select element
*/
public List getOptions() {
return Collections.unmodifiableList(this.getElementsByTagNameImpl("option"));
}
/**
* Returns the indexed option.
*
* @param index the index
* @return the option specified by the index
*/
public HtmlOption getOption(final int index) {
return this.getElementsByTagNameImpl("option").get(index);
}
/**
* Returns the number of options.
* @return the number of options
*/
public int getOptionSize() {
return getElementsByTagName("option").size();
}
/**
* Remove options by reducing the "length" property. This has no
* effect if the length is set to the same or greater.
* @param newLength the new length property value
*/
public void setOptionSize(final int newLength) {
final List elementList = getElementsByTagName("option");
for (int i = elementList.size() - 1; i >= newLength; i--) {
elementList.get(i).remove();
}
}
/**
* Remove an option at the given index.
* @param index the index of the option to remove
*/
public void removeOption(final int index) {
final ChildElementsIterator iterator = new ChildElementsIterator(this);
for (int i = 0; iterator.hasNext();) {
final DomElement element = iterator.next();
if (element instanceof HtmlOption) {
if (i == index) {
element.remove();
ensureSelectedIndex();
return;
}
i++;
}
}
}
/**
* Replace an option at the given index with a new option.
* @param index the index of the option to remove
* @param newOption the new option to replace to indexed option
*/
public void replaceOption(final int index, final HtmlOption newOption) {
final ChildElementsIterator iterator = new ChildElementsIterator(this);
for (int i = 0; iterator.hasNext();) {
final DomElement element = iterator.next();
if (element instanceof HtmlOption) {
if (i == index) {
element.replace(newOption);
ensureSelectedIndex();
return;
}
i++;
}
}
if (newOption.isSelected()) {
setSelectedAttribute(newOption, true);
}
}
/**
* Add a new option at the end.
* @param newOption the new option to add
*/
public void appendOption(final HtmlOption newOption) {
appendChild(newOption);
ensureSelectedIndex();
}
/**
* {@inheritDoc}
*/
@Override
public DomNode appendChild(final Node node) {
final DomNode response = super.appendChild(node);
if (node instanceof HtmlOption) {
final HtmlOption option = (HtmlOption) node;
if (option.isSelected()) {
doSelectOption(option, true, false, false, false);
}
}
return response;
}
/**
* Sets the "selected" state of the specified option. If this "select" element
* is single-select, then calling this method will deselect all other options.
*
* Only options that are actually in the document may be selected.
*
* @param isSelected true if the option is to become selected
* @param optionValue the value of the option that is to change
* @param
the page type
* @return the page contained in the current window as returned
* by {@link org.htmlunit.WebClient#getCurrentWindow()}
*/
public
P setSelectedAttribute(final String optionValue, final boolean isSelected) {
return setSelectedAttribute(optionValue, isSelected, true);
}
/**
* INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*
* Sets the "selected" state of the specified option. If this "select" element
* is single-select, then calling this method will deselect all other options.
*
* Only options that are actually in the document may be selected.
*
* @param isSelected true if the option is to become selected
* @param optionValue the value of the option that is to change
* @param invokeOnFocus whether to set focus or not.
* @param
the page type
* @return the page contained in the current window as returned
* by {@link org.htmlunit.WebClient#getCurrentWindow()}
*/
@SuppressWarnings("unchecked")
public
P setSelectedAttribute(final String optionValue,
final boolean isSelected, final boolean invokeOnFocus) {
try {
final HtmlOption selected = getOptionByValue(optionValue);
return setSelectedAttribute(selected, isSelected, invokeOnFocus, true, false, true);
}
catch (final ElementNotFoundException e) {
for (final HtmlOption o : getSelectedOptions()) {
o.setSelected(false);
}
return (P) getPage();
}
}
/**
* Sets the "selected" state of the specified option. If this "select" element
* is single-select, then calling this method will deselect all other options.
*
* Only options that are actually in the document may be selected.
*
* @param isSelected true if the option is to become selected
* @param selectedOption the value of the option that is to change
* @param
the page type
* @return the page contained in the current window as returned
* by {@link org.htmlunit.WebClient#getCurrentWindow()}
*/
public
P setSelectedAttribute(final HtmlOption selectedOption, final boolean isSelected) {
return setSelectedAttribute(selectedOption, isSelected, true, true, false, true);
}
/**
* INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*
* Sets the "selected" state of the specified option. If this "select" element
* is single-select, then calling this method will deselect all other options.
*
* Only options that are actually in the document may be selected.
*
* @param isSelected true if the option is to become selected
* @param selectedOption the value of the option that is to change
* @param invokeOnFocus whether to set focus or not.
* @param shiftKey {@code true} if SHIFT is pressed
* @param ctrlKey {@code true} if CTRL is pressed
* @param isClick is mouse clicked
* @param
the page type
* @return the page contained in the current window as returned
* by {@link org.htmlunit.WebClient#getCurrentWindow()}
*/
@SuppressWarnings("unchecked")
public
P setSelectedAttribute(final HtmlOption selectedOption, final boolean isSelected,
final boolean invokeOnFocus, final boolean shiftKey, final boolean ctrlKey, final boolean isClick) {
if (isSelected && invokeOnFocus) {
((HtmlPage) getPage()).setFocusedElement(this);
}
final boolean changeSelectedState = selectedOption.isSelected() != isSelected;
if (changeSelectedState) {
doSelectOption(selectedOption, isSelected, shiftKey, ctrlKey, isClick);
HtmlInput.executeOnChangeHandlerIfAppropriate(this);
}
return (P) getPage().getWebClient().getCurrentWindow().getEnclosedPage();
}
private void doSelectOption(final HtmlOption selectedOption,
final boolean isSelected, final boolean shiftKey, final boolean ctrlKey, final boolean isClick) {
// caution the HtmlOption may have been created from js and therefore the select now need
// to "know" that it is selected
if (isMultipleSelectEnabled()) {
selectedOption.setSelectedInternal(isSelected);
if (isClick && !ctrlKey) {
if (!shiftKey) {
setOnlySelected(selectedOption, isSelected);
lastSelectedIndex_ = getOptions().indexOf(selectedOption);
}
else if (isSelected && lastSelectedIndex_ != -1) {
final List options = getOptions();
final int newIndex = options.indexOf(selectedOption);
for (int i = 0; i < options.size(); i++) {
options.get(i).setSelectedInternal(isBetween(i, lastSelectedIndex_, newIndex));
}
}
}
}
else {
setOnlySelected(selectedOption, isSelected);
}
}
/**
* Sets the given {@link HtmlOption} as the only selected one.
* @param selectedOption the selected {@link HtmlOption}
* @param isSelected whether selected or not
*/
void setOnlySelected(final HtmlOption selectedOption, final boolean isSelected) {
for (final HtmlOption option : getOptions()) {
option.setSelectedInternal(option == selectedOption && isSelected);
}
}
private static boolean isBetween(final int number, final int min, final int max) {
return max > min ? number >= min && number <= max : number >= max && number <= min;
}
/**
* {@inheritDoc}
*/
@Override
public NameValuePair[] getSubmitNameValuePairs() {
final String name = getNameAttribute();
final List selectedOptions = getSelectedOptions();
final NameValuePair[] pairs = new NameValuePair[selectedOptions.size()];
int i = 0;
for (final HtmlOption option : selectedOptions) {
pairs[i++] = new NameValuePair(name, option.getValueAttribute());
}
return pairs;
}
/**
* Indicates if this select is submittable
* @return {@code false} if not
*/
boolean isValidForSubmission() {
return getOptionSize() > 0;
}
/**
* Returns the value of this element to what it was at the time the page was loaded.
*/
@Override
public void reset() {
for (final HtmlOption option : getOptions()) {
option.reset();
}
onAllChildrenAddedToPage(false);
}
/**
* {@inheritDoc}
* @see SubmittableElement#setDefaultValue(String)
*/
@Override
public void setDefaultValue(final String defaultValue) {
setSelectedAttribute(defaultValue, true);
}
/**
* {@inheritDoc}
* @see SubmittableElement#setDefaultValue(String)
*/
@Override
public String getDefaultValue() {
final List options = getSelectedOptions();
if (options.isEmpty()) {
return "";
}
return options.get(0).getValueAttribute();
}
/**
* {@inheritDoc}
* This implementation is empty; only checkboxes and radio buttons
* really care what the default checked value is.
* @see SubmittableElement#setDefaultChecked(boolean)
* @see HtmlRadioButtonInput#setDefaultChecked(boolean)
* @see HtmlCheckBoxInput#setDefaultChecked(boolean)
*/
@Override
public void setDefaultChecked(final boolean defaultChecked) {
// Empty.
}
/**
* {@inheritDoc}
* This implementation returns {@code false}; only checkboxes and
* radio buttons really care what the default checked value is.
* @see SubmittableElement#isDefaultChecked()
* @see HtmlRadioButtonInput#isDefaultChecked()
* @see HtmlCheckBoxInput#isDefaultChecked()
*/
@Override
public boolean isDefaultChecked() {
return false;
}
/**
* Returns {@code true} if this select is using "multiple select".
* @return {@code true} if this select is using "multiple select"
*/
public boolean isMultipleSelectEnabled() {
return getAttributeDirect("multiple") != ATTRIBUTE_NOT_DEFINED;
}
/**
* Returns the {@link HtmlOption} object that corresponds to the specified value.
*
* @param value the value to search by
* @return the {@link HtmlOption} object that corresponds to the specified value
* @exception ElementNotFoundException If a particular element could not be found in the DOM model
*/
public HtmlOption getOptionByValue(final String value) throws ElementNotFoundException {
WebAssert.notNull(VALUE_ATTRIBUTE, value);
for (final HtmlOption option : getOptions()) {
if (option.getValueAttribute().equals(value)) {
return option;
}
}
throw new ElementNotFoundException("option", VALUE_ATTRIBUTE, value);
}
/**
* Returns the {@link HtmlOption} object that has the specified text.
*
* @param text the text to search by
* @return the {@link HtmlOption} object that has the specified text
* @exception ElementNotFoundException If a particular element could not be found in the DOM model
*/
public HtmlOption getOptionByText(final String text) throws ElementNotFoundException {
WebAssert.notNull("text", text);
for (final HtmlOption option : getOptions()) {
if (option.getText().equals(text)) {
return option;
}
}
throw new ElementNotFoundException("option", "text", text);
}
/**
* Returns the value of the attribute {@code name}. Refer to the HTML 4.01 documentation for details on the use of this attribute.
*
* @return the value of the attribute {@code name} or an empty string if that attribute isn't defined
*/
public final String getNameAttribute() {
return getAttributeDirect(NAME_ATTRIBUTE);
}
/**
* Returns the value of the attribute {@code size}. Refer to the HTML 4.01 documentation for
* details on the use of this attribute.
*
* @return the value of the attribute {@code size} or an empty string if that attribute isn't defined
*/
public final String getSizeAttribute() {
return getAttributeDirect("size");
}
/**
* @return the size or 1 if not defined or not convertable to int
*/
public final int getSize() {
int size = 0;
final String sizeAttribute = getSizeAttribute();
if (ATTRIBUTE_NOT_DEFINED != sizeAttribute && ATTRIBUTE_VALUE_EMPTY != sizeAttribute) {
try {
size = Integer.parseInt(sizeAttribute);
}
catch (final Exception ignored) {
// silently ignore
}
}
return size;
}
/**
* Returns the value of the attribute {@code multiple}. Refer to the HTML 4.01 documentation for details on the use of this attribute.
*
* @return the value of the attribute {@code multiple} or an empty string if that attribute isn't defined
*/
public final String getMultipleAttribute() {
return getAttributeDirect("multiple");
}
/**
* {@inheritDoc}
*/
@Override
public final String getDisabledAttribute() {
return getAttributeDirect(ATTRIBUTE_DISABLED);
}
/**
* {@inheritDoc}
*/
@Override
public final boolean isDisabled() {
if (hasAttribute(ATTRIBUTE_DISABLED)) {
return true;
}
Node node = getParentNode();
while (node != null) {
if (node instanceof DisabledElement
&& ((DisabledElement) node).isDisabled()) {
return true;
}
node = node.getParentNode();
}
return false;
}
/**
* Returns {@code true} if this element is read only.
* @return {@code true} if this element is read only
*/
public boolean isReadOnly() {
return hasAttribute("readOnly");
}
/**
* Returns the value of the attribute {@code tabindex}. Refer to the HTML 4.01 documentation for details on the use of this attribute.
*
* @return the value of the attribute {@code tabindex} or an empty string if that attribute isn't defined
*/
public final String getTabIndexAttribute() {
return getAttributeDirect("tabindex");
}
/**
* Returns the value of the attribute {@code onfocus}. Refer to the HTML 4.01 documentation for details on the use of this attribute.
*
* @return the value of the attribute {@code onfocus} or an empty string if that attribute isn't defined
*/
public final String getOnFocusAttribute() {
return getAttributeDirect("onfocus");
}
/**
* Returns the value of the attribute {@code onblur}. Refer to the HTML 4.01 documentation for details on the use of this attribute.
*
* @return the value of the attribute {@code onblur} or an empty string if that attribute isn't defined
*/
public final String getOnBlurAttribute() {
return getAttributeDirect("onblur");
}
/**
* Returns the value of the attribute {@code onchange}. Refer to the HTML 4.01 documentation for details on the use of this attribute.
*
* @return the value of the attribute {@code onchange} or an empty string if that attribute isn't defined
*/
public final String getOnChangeAttribute() {
return getAttributeDirect("onchange");
}
/**
* {@inheritDoc}
*/
@Override
protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue,
final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) {
final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName);
if (DomElement.NAME_ATTRIBUTE.equals(qualifiedNameLC)) {
if (newNames_.isEmpty()) {
newNames_ = new HashSet<>();
}
newNames_.add(attributeValue);
}
super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners,
notifyMutationObservers);
}
/**
* {@inheritDoc}
*/
@Override
public String getOriginalName() {
return originalName_;
}
/**
* {@inheritDoc}
*/
@Override
public Collection getNewNames() {
return newNames_;
}
/**
* {@inheritDoc}
*/
@Override
public DisplayStyle getDefaultStyleDisplay() {
return DisplayStyle.INLINE_BLOCK;
}
/**
* Returns the value of the {@code selectedIndex} property.
* @return the selectedIndex property
*/
public int getSelectedIndex() {
final List selectedOptions = getSelectedOptions();
if (selectedOptions.isEmpty()) {
return -1;
}
final List allOptions = getOptions();
return allOptions.indexOf(selectedOptions.get(0));
}
/**
* Sets the value of the {@code selectedIndex} property.
* @param index the new value
*/
public void setSelectedIndex(final int index) {
for (final HtmlOption itemToUnSelect : getSelectedOptions()) {
setSelectedAttribute(itemToUnSelect, false);
}
if (index < 0) {
return;
}
final List allOptions = getOptions();
if (index < allOptions.size()) {
final HtmlOption itemToSelect = allOptions.get(index);
setSelectedAttribute(itemToSelect, true, false, true, false, true);
}
}
/**
* INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*
* Resets the selectedIndex if needed.
*/
public void ensureSelectedIndex() {
if (getOptionSize() == 0) {
setSelectedIndex(-1);
}
else if (getSelectedIndex() == -1 && !isMultipleSelectEnabled()) {
setSelectedIndex(0);
}
}
/**
* INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.
*
* @param option the option to search for
* @return the index of the provided option or zero if not found
*/
public int indexOf(final HtmlOption option) {
if (option == null) {
return 0;
}
int index = 0;
for (final HtmlElement element : getHtmlElementDescendants()) {
if (option == element) {
return index;
}
index++;
}
return 0;
}
/**
* {@inheritDoc}
*/
@Override
protected boolean isRequiredSupported() {
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean willValidate() {
return !isDisabled() && (hasFeature(HTMLSELECT_WILL_VALIDATE_IGNORES_READONLY) || !isReadOnly());
}
/**
* {@inheritDoc}
*/
@Override
public void setCustomValidity(final String message) {
customValidity_ = message;
}
/**
* {@inheritDoc}
*/
@Override
public boolean isValid() {
return isValidValidityState();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isCustomErrorValidityState() {
return !StringUtils.isEmpty(customValidity_);
}
@Override
public boolean isValidValidityState() {
return !isCustomErrorValidityState()
&& !isValueMissingValidityState();
}
/**
* {@inheritDoc}
*/
@Override
public boolean isValueMissingValidityState() {
return ATTRIBUTE_NOT_DEFINED != getAttributeDirect(ATTRIBUTE_REQUIRED)
&& getSelectedOptions().isEmpty();
}
}