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 }