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