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