All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.htmlunit.html.HtmlSelect Maven / Gradle / Ivy

The newest version!
/*
 * 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(); } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy