View Javadoc
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.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 Mike Bowler
32   * @author David K. Taylor
33   * @author Christian Sell
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 option) {
126             if (option.isSelected()) {
127                 getEnclosingSelect().setSelectedAttribute(option, true);
128             }
129         }
130     }
131 
132     /**
133      * Gets the enclosing select of this option.
134      * @return {@code null} if no select is found (for instance malformed HTML)
135      */
136     public HtmlSelect getEnclosingSelect() {
137         return (HtmlSelect) getEnclosingElement(HtmlSelect.TAG_NAME);
138     }
139 
140     /**
141      * Resets the option to its original selected state.
142      */
143     public void reset() {
144         setSelectedInternal(hasAttribute("selected"));
145     }
146 
147     /**
148      * Returns the value of the attribute {@code selected}. Refer to the
149      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
150      * documentation for details on the use of this attribute.
151      *
152      * @return the value of the attribute {@code selected}
153      *         or an empty string if that attribute isn't defined.
154      */
155     public final String getSelectedAttribute() {
156         return getAttributeDirect("selected");
157     }
158 
159     /**
160      * Returns whether this Option is selected by default.
161      * That is whether the "selected"
162      * attribute exists when the Option is constructed. This also determines
163      * the value of getSelectedAttribute() after a reset() on the form.
164      * @return whether the option is selected by default
165      */
166     public final boolean isDefaultSelected() {
167         return hasAttribute("selected");
168     }
169 
170     /**
171      * Returns whether this element is disabled.
172      *
173      * @return {@code true} if this element is disabled, either because it has
174      *         the {@code disabled} attribute or because it is contained in a
175      *         disabled ancestor element
176      */
177     @Override
178     public final boolean isDisabled() {
179         if (hasAttribute(ATTRIBUTE_DISABLED)) {
180             return true;
181         }
182 
183         Node node = getParentNode();
184         while (node != null) {
185             if (node instanceof DisabledElement element
186                     && element.isDisabled()) {
187                 return true;
188             }
189             node = node.getParentNode();
190         }
191 
192         return false;
193     }
194 
195     /**
196      * {@inheritDoc}
197      */
198     @Override
199     public final String getDisabledAttribute() {
200         return getAttributeDirect(ATTRIBUTE_DISABLED);
201     }
202 
203     /**
204      * Returns the value of the attribute {@code label}. Refer to the
205      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
206      * documentation for details on the use of this attribute.
207      *
208      * @return the value of the attribute {@code label} or an empty string if that attribute isn't defined
209      */
210     public final String getLabelAttribute() {
211         return getAttributeDirect("label");
212     }
213 
214     /**
215      * Sets the value of the attribute {@code label}. Refer to the
216      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
217      * documentation for details on the use of this attribute.
218      *
219      * @param newLabel the value of the attribute {@code label}
220      */
221     public final void setLabelAttribute(final String newLabel) {
222         setAttribute("label", newLabel);
223     }
224 
225     /**
226      * Returns the value of the attribute {@code value}. Refer to the
227      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
228      * documentation for details on the use of this attribute.
229      * @see <a href="http://www.w3.org/TR/1999/REC-html401-19991224/interact/forms.html#adef-value-OPTION">
230      *     initial value if value attribute is not set</a>
231      * @return the value of the attribute {@code value}
232      */
233     public final String getValueAttribute() {
234         String value = getAttributeDirect(VALUE_ATTRIBUTE);
235         if (ATTRIBUTE_NOT_DEFINED == value) {
236             value = getText();
237         }
238         return value;
239     }
240 
241     /**
242      * Sets the value of the attribute {@code value}. Refer to the
243      * <a href="http://www.w3.org/TR/html401/">HTML 4.01</a>
244      * documentation for details on the use of this attribute.
245      *
246      * @param newValue the value of the attribute {@code value}
247      */
248     public final void setValueAttribute(final String newValue) {
249         setAttribute(VALUE_ATTRIBUTE, newValue);
250     }
251 
252     /**
253      * Selects the option if it's not already selected.
254      * {@inheritDoc}
255      */
256     @Override
257     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
258         boolean changed = false;
259         if (!isSelected()) {
260             setSelected(true, true, true, shiftKey, ctrlKey);
261             changed = true;
262         }
263         else if (getEnclosingSelect().isMultipleSelectEnabled()) {
264             if (ctrlKey) {
265                 setSelected(false, true, true, shiftKey, ctrlKey);
266                 changed = true;
267             }
268             else {
269                 getEnclosingSelect().setOnlySelected(this, true);
270             }
271         }
272         super.doClickStateUpdate(shiftKey, ctrlKey);
273         return changed;
274     }
275 
276     /**
277      * {@inheritDoc}
278      */
279     @Override
280     protected boolean isStateUpdateFirst() {
281         return true;
282     }
283 
284     /**
285      * {@inheritDoc}
286      */
287     @Override
288     protected void printOpeningTagContentAsXml(final PrintWriter printWriter) {
289         super.printOpeningTagContentAsXml(printWriter);
290         if (selected_ && getAttributeDirect("selected") == ATTRIBUTE_NOT_DEFINED) {
291             printWriter.print(" selected=\"selected\"");
292         }
293     }
294 
295     /**
296      * For internal use only.
297      * Sets/remove the selected attribute to reflect the select state
298      * @param selected the selected status
299      */
300     void setSelectedInternal(final boolean selected) {
301         selected_ = selected;
302     }
303 
304     /**
305      * Sets the text for this HtmlOption.
306      * @param text the text
307      */
308     public void setText(final String text) {
309         if (text == null || text.isEmpty()) {
310             removeAllChildren();
311         }
312         else {
313             final DomNode child = getFirstChild();
314             if (child == null) {
315                 appendChild(new DomText(getPage(), text));
316             }
317             else {
318                 child.setNodeValue(text);
319             }
320         }
321     }
322 
323     /**
324      * Gets the text.
325      * @return the text of this option.
326      */
327     public String getText() {
328         final HtmlSerializerNormalizedText ser = new HtmlSerializerNormalizedText();
329         ser.setIgnoreMaskedElements(false);
330         return ser.asText(this);
331     }
332 
333     /**
334      * {@inheritDoc}
335      */
336     @Override
337     public Page mouseOver(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
338         // to move the mouse over the oution we will touch the select (border)
339         // depending on your mous speed and the browser this event is triggered or not
340         getEnclosingSelect().mouseOver(shiftKey, ctrlKey, altKey, button);
341 
342         return super.mouseOver(shiftKey, ctrlKey, altKey, button);
343     }
344 
345     /**
346      * {@inheritDoc}
347      */
348     @Override
349     public DisplayStyle getDefaultStyleDisplay() {
350         return DisplayStyle.BLOCK;
351     }
352 
353     /**
354      * {@inheritDoc}
355      */
356     @Override
357     public boolean handles(final Event event) {
358         if (MouseEvent.TYPE_MOUSE_OVER.equals(event.getType())) {
359             return true;
360         }
361         return super.handles(event);
362     }
363 
364     /**
365      * {@inheritDoc}
366      */
367     @Override
368     protected void basicRemove() {
369         final DomNode parent = getParentNode();
370         super.basicRemove();
371 
372         if (parent != null && isSelected()) {
373             // update selection and size if needed
374             parent.onAllChildrenAddedToPage(false);
375         }
376     }
377 }