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 }