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