View Javadoc
1   /*
2    * Copyright (c) 2002-2025 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.io.IOException;
18  import java.io.PrintWriter;
19  import java.util.Map;
20  
21  import org.htmlunit.Page;
22  import org.htmlunit.SgmlPage;
23  import org.htmlunit.html.serializer.HtmlSerializerNormalizedText;
24  import org.htmlunit.javascript.host.event.Event;
25  import org.htmlunit.javascript.host.event.MouseEvent;
26  import org.w3c.dom.Node;
27  
28  /**
29   * Wrapper for the HTML element "option".
30   *
31   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
32   * @author David K. Taylor
33   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
34   * @author David D. Kilzer
35   * @author Marc Guillemot
36   * @author Ahmed Ashour
37   * @author Daniel Gredler
38   * @author Ronald Brill
39   * @author Frank Danek
40   */
41  public class HtmlOption extends HtmlElement implements DisabledElement {
42  
43      /** The HTML tag represented by this element. */
44      public static final String TAG_NAME = "option";
45  
46      private boolean selected_;
47  
48      /**
49       * Creates an instance.
50       *
51       * @param qualifiedName the qualified name of the element type to instantiate
52       * @param page the page that contains this element
53       * @param attributes the initial attributes
54       */
55      HtmlOption(final String qualifiedName, final SgmlPage page,
56              final Map<String, DomAttr> attributes) {
57          super(qualifiedName, page, attributes);
58          reset();
59      }
60  
61      /**
62       * Returns {@code true} if this option is currently selected.
63       * @return {@code true} if this option is currently selected
64       */
65      public boolean isSelected() {
66          return selected_;
67      }
68  
69      /**
70       * Sets the selected state of this option. This will possibly also change the
71       * selected properties of sibling option elements.
72       *
73       * @param selected true if this option should be selected
74       * @return the page that occupies this window after this change is made (may or
75       *         may not be the same as the original page)
76       */
77      public Page setSelected(final boolean selected) {
78          setSelected(selected, true, false, false, false);
79          return getPage();
80      }
81  
82      /**
83       * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
84       *
85       * Sets the selected state of this option. This will possibly also change the
86       * selected properties of sibling option elements.
87       *
88       * @param selected true if this option should be selected
89       */
90      public void setSelectedFromJavaScript(final boolean selected) {
91          setSelected(selected, false, false, true, false);
92      }
93  
94      /**
95       * Sets the selected state of this option. This will possibly also change the
96       * selected properties of sibling option elements.
97       *
98       * @param selected true if this option should be selected
99       * @param invokeOnFocus whether to set focus or not.
100      * @param isClick is mouse clicked
101      * @param shiftKey {@code true} if SHIFT is pressed
102      * @param ctrlKey {@code true} if CTRL is pressed
103      */
104     private void setSelected(final boolean selected, final boolean invokeOnFocus, final boolean isClick,
105             final boolean shiftKey, final boolean ctrlKey) {
106         if (selected == isSelected()) {
107             return;
108         }
109         final HtmlSelect select = getEnclosingSelect();
110         if (select != null) {
111             select.setSelectedAttribute(this, selected, invokeOnFocus, shiftKey, ctrlKey, isClick);
112             return;
113         }
114         // for instance from JS for an option created by document.createElement('option')
115         // and not yet added to a select
116         setSelectedInternal(selected);
117     }
118 
119     /**
120      * {@inheritDoc}
121      */
122     @Override
123     public void insertBefore(final DomNode newNode) {
124         super.insertBefore(newNode);
125         if (newNode instanceof HtmlOption) {
126             final HtmlOption option = (HtmlOption) newNode;
127             if (option.isSelected()) {
128                 getEnclosingSelect().setSelectedAttribute(option, true);
129             }
130         }
131     }
132 
133     /**
134      * Gets the enclosing select of this option.
135      * @return {@code null} if no select is found (for instance malformed HTML)
136      */
137     public HtmlSelect getEnclosingSelect() {
138         return (HtmlSelect) getEnclosingElement(HtmlSelect.TAG_NAME);
139     }
140 
141     /**
142      * Resets the option to its original selected state.
143      */
144     public void reset() {
145         setSelectedInternal(hasAttribute("selected"));
146     }
147 
148     /**
149      * Returns the value of the attribute {@code selected}. Refer to the
150      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
151      * documentation for details on the use of this attribute.
152      *
153      * @return the value of the attribute {@code selected}
154      *         or an empty string if that attribute isn't defined.
155      */
156     public final String getSelectedAttribute() {
157         return getAttributeDirect("selected");
158     }
159 
160     /**
161      * Returns whether this Option is selected by default.
162      * That is whether the "selected"
163      * attribute exists when the Option is constructed. This also determines
164      * the value of getSelectedAttribute() after a reset() on the form.
165      * @return whether the option is selected by default
166      */
167     public final boolean isDefaultSelected() {
168         return hasAttribute("selected");
169     }
170 
171     /**
172      * @return {@code true} if the disabled attribute is set for this element
173      */
174     @Override
175     public final boolean isDisabled() {
176         if (hasAttribute(ATTRIBUTE_DISABLED)) {
177             return true;
178         }
179 
180         Node node = getParentNode();
181         while (node != null) {
182             if (node instanceof DisabledElement
183                     && ((DisabledElement) node).isDisabled()) {
184                 return true;
185             }
186             node = node.getParentNode();
187         }
188 
189         return false;
190     }
191 
192     /**
193      * {@inheritDoc}
194      */
195     @Override
196     public final String getDisabledAttribute() {
197         return getAttributeDirect(ATTRIBUTE_DISABLED);
198     }
199 
200     /**
201      * Returns the value of the attribute {@code label}. Refer to the
202      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
203      * documentation for details on the use of this attribute.
204      *
205      * @return the value of the attribute {@code label} or an empty string if that attribute isn't defined
206      */
207     public final String getLabelAttribute() {
208         return getAttributeDirect("label");
209     }
210 
211     /**
212      * Sets the value of the attribute {@code label}. Refer to the
213      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
214      * documentation for details on the use of this attribute.
215      *
216      * @param newLabel the value of the attribute {@code label}
217      */
218     public final void setLabelAttribute(final String newLabel) {
219         setAttribute("label", newLabel);
220     }
221 
222     /**
223      * Returns the value of the attribute {@code value}. Refer to the
224      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
225      * documentation for details on the use of this attribute.
226      * @see <a href="http://www.w3.org/TR/1999/REC-html401-19991224/interact/forms.html#adef-value-OPTION">
227      * initial value if value attribute is not set</a>
228      * @return the value of the attribute {@code value}
229      */
230     public final String getValueAttribute() {
231         String value = getAttributeDirect(VALUE_ATTRIBUTE);
232         if (ATTRIBUTE_NOT_DEFINED == value) {
233             value = getText();
234         }
235         return value;
236     }
237 
238     /**
239      * Sets the value of the attribute {@code value}. Refer to the
240      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
241      * documentation for details on the use of this attribute.
242      *
243      * @param newValue the value of the attribute {@code value}
244      */
245     public final void setValueAttribute(final String newValue) {
246         setAttribute(VALUE_ATTRIBUTE, newValue);
247     }
248 
249     /**
250      * Selects the option if it's not already selected.
251      * {@inheritDoc}
252      */
253     @Override
254     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
255         boolean changed = false;
256         if (!isSelected()) {
257             setSelected(true, true, true, shiftKey, ctrlKey);
258             changed = true;
259         }
260         else if (getEnclosingSelect().isMultipleSelectEnabled()) {
261             if (ctrlKey) {
262                 setSelected(false, true, true, shiftKey, ctrlKey);
263                 changed = true;
264             }
265             else {
266                 getEnclosingSelect().setOnlySelected(this, true);
267             }
268         }
269         super.doClickStateUpdate(shiftKey, ctrlKey);
270         return changed;
271     }
272 
273     /**
274      * {@inheritDoc}
275      */
276     @Override
277     protected boolean isStateUpdateFirst() {
278         return true;
279     }
280 
281     /**
282      * {@inheritDoc}
283      */
284     @Override
285     protected void printOpeningTagContentAsXml(final PrintWriter printWriter) {
286         super.printOpeningTagContentAsXml(printWriter);
287         if (selected_ && getAttributeDirect("selected") == ATTRIBUTE_NOT_DEFINED) {
288             printWriter.print(" selected=\"selected\"");
289         }
290     }
291 
292     /**
293      * For internal use only.
294      * Sets/remove the selected attribute to reflect the select state
295      * @param selected the selected status
296      */
297     void setSelectedInternal(final boolean selected) {
298         selected_ = selected;
299     }
300 
301     /**
302      * Sets the text for this HtmlOption.
303      * @param text the text
304      */
305     public void setText(final String text) {
306         if (text == null || text.isEmpty()) {
307             removeAllChildren();
308         }
309         else {
310             final DomNode child = getFirstChild();
311             if (child == null) {
312                 appendChild(new DomText(getPage(), text));
313             }
314             else {
315                 child.setNodeValue(text);
316             }
317         }
318     }
319 
320     /**
321      * Gets the text.
322      * @return the text of this option.
323      */
324     public String getText() {
325         final HtmlSerializerNormalizedText ser = new HtmlSerializerNormalizedText();
326         ser.setIgnoreMaskedElements(false);
327         return ser.asText(this);
328     }
329 
330     /**
331      * {@inheritDoc}
332      */
333     @Override
334     public Page mouseOver(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
335         // to move the mouse over the oution we will touch the select (border)
336         // depending on your mous speed and the browser this event is triggered or not
337         getEnclosingSelect().mouseOver(shiftKey, ctrlKey, altKey, button);
338 
339         return super.mouseOver(shiftKey, ctrlKey, altKey, button);
340     }
341 
342     /**
343      * {@inheritDoc}
344      */
345     @Override
346     public DisplayStyle getDefaultStyleDisplay() {
347         return DisplayStyle.BLOCK;
348     }
349 
350     /**
351      * {@inheritDoc}
352      */
353     @Override
354     public boolean handles(final Event event) {
355         if (MouseEvent.TYPE_MOUSE_OVER.equals(event.getType())) {
356             return true;
357         }
358         return super.handles(event);
359     }
360 
361     /**
362      * {@inheritDoc}
363      */
364     @Override
365     protected void basicRemove() {
366         final DomNode parent = getParentNode();
367         super.basicRemove();
368 
369         if (parent != null && isSelected()) {
370             // update selection and size if needed
371             parent.onAllChildrenAddedToPage(false);
372         }
373     }
374 }