View Javadoc
1   /*
2    * Copyright (c) 2002-2018 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    * http://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 com.gargoylesoftware.htmlunit.html;
16  
17  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.CSS_DISPLAY_BLOCK2;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONCLICK_FOR_SELECT_ONLY;
19  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONMOUSEDOWN_FOR_SELECT_OPTION_TRIGGERS_ADDITIONAL_DOWN_FOR_SELECT;
20  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONMOUSEDOWN_NOT_FOR_SELECT_OPTION;
21  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONMOUSEOVER_FOR_DISABLED_OPTION;
22  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONMOUSEOVER_NEVER_FOR_SELECT_OPTION;
23  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONMOUSEUP_FOR_SELECT_OPTION_TRIGGERS_ADDITIONAL_UP_FOR_SELECT;
24  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.EVENT_ONMOUSEUP_NOT_FOR_SELECT_OPTION;
25  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLOPTION_EXACT_ONE_OPTION_GETS_NERVER_DESELECTED;
26  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLOPTION_PREVENT_DISABLED;
27  
28  import java.io.IOException;
29  import java.io.PrintWriter;
30  import java.util.Map;
31  
32  import com.gargoylesoftware.htmlunit.Page;
33  import com.gargoylesoftware.htmlunit.SgmlPage;
34  import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
35  import com.gargoylesoftware.htmlunit.javascript.host.event.MouseEvent;
36  
37  /**
38   * Wrapper for the HTML element "option".
39   *
40   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
41   * @author David K. Taylor
42   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
43   * @author David D. Kilzer
44   * @author Marc Guillemot
45   * @author Ahmed Ashour
46   * @author Daniel Gredler
47   * @author Ronald Brill
48   * @author Frank Danek
49   */
50  public class HtmlOption extends HtmlElement implements DisabledElement {
51  
52      /** The HTML tag represented by this element. */
53      public static final String TAG_NAME = "option";
54  
55      private boolean selected_;
56  
57      /**
58       * Creates an instance.
59       *
60       * @param qualifiedName the qualified name of the element type to instantiate
61       * @param page the page that contains this element
62       * @param attributes the initial attributes
63       */
64      HtmlOption(final String qualifiedName, final SgmlPage page,
65              final Map<String, DomAttr> attributes) {
66          super(qualifiedName, page, attributes);
67          reset();
68      }
69  
70      /**
71       * Returns {@code true} if this option is currently selected.
72       * @return {@code true} if this option is currently selected
73       */
74      public boolean isSelected() {
75          return selected_;
76      }
77  
78      /**
79       * Sets the selected state of this option. This will possibly also change the
80       * selected properties of sibling option elements.
81       *
82       * @param selected true if this option should be selected
83       * @return the page that occupies this window after this change is made (may or
84       *         may not be the same as the original page)
85       */
86      public Page setSelected(final boolean selected) {
87          setSelected(selected, true, false, false, false);
88          return getPage();
89      }
90  
91      /**
92       * <span style="color:red">INTERNAL API - SUBJECT TO CHANGE AT ANY TIME - USE AT YOUR OWN RISK.</span><br>
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       */
99      public void setSelectedFromJavaScript(final boolean selected) {
100         setSelected(selected, false, false, true, false);
101     }
102 
103     /**
104      * Sets the selected state of this option. This will possibly also change the
105      * selected properties of sibling option elements.
106      *
107      * @param selected true if this option should be selected
108      * @param invokeOnFocus whether to set focus or not.
109      * @param isClick is mouse clicked
110      * @param shiftKey {@code true} if SHIFT is pressed
111      * @param ctrlKey {@code true} if CTRL is pressed
112      */
113     private void setSelected(boolean selected, final boolean invokeOnFocus, final boolean isClick,
114             final boolean shiftKey, final boolean ctrlKey) {
115         if (selected == isSelected()) {
116             return;
117         }
118         final HtmlSelect select = getEnclosingSelect();
119         if (select != null) {
120             if (hasFeature(HTMLOPTION_EXACT_ONE_OPTION_GETS_NERVER_DESELECTED)
121                     && !select.isMultipleSelectEnabled() && select.getOptionSize() == 1) {
122                 selected = true;
123             }
124             select.setSelectedAttribute(this, selected, invokeOnFocus, shiftKey, ctrlKey, isClick);
125             return;
126         }
127         // for instance from JS for an option created by document.createElement('option')
128         // and not yet added to a select
129         setSelectedInternal(selected);
130     }
131 
132     /**
133      * {@inheritDoc}
134      */
135     @Override
136     public void insertBefore(final DomNode newNode) {
137         super.insertBefore(newNode);
138         if (newNode instanceof HtmlOption) {
139             final HtmlOption option = (HtmlOption) newNode;
140             if (option.isSelected()) {
141                 getEnclosingSelect().setSelectedAttribute(option, true);
142             }
143         }
144     }
145 
146     /**
147      * Gets the enclosing select of this option.
148      * @return {@code null} if no select is found (for instance malformed HTML)
149      */
150     public HtmlSelect getEnclosingSelect() {
151         return (HtmlSelect) getEnclosingElement(HtmlSelect.TAG_NAME);
152     }
153 
154     /**
155      * Resets the option to its original selected state.
156      */
157     public void reset() {
158         setSelectedInternal(hasAttribute("selected"));
159     }
160 
161     /**
162      * Returns the value of the attribute {@code selected}. Refer to the
163      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
164      * documentation for details on the use of this attribute.
165      *
166      * @return the value of the attribute {@code selected}
167      * or an empty string if that attribute isn't defined.
168      */
169     public final String getSelectedAttribute() {
170         return getAttributeDirect("selected");
171     }
172 
173     /**
174      * Returns whether this Option is selected by default.
175      * That is whether the "selected"
176      * attribute exists when the Option is constructed. This also determines
177      * the value of getSelectedAttribute() after a reset() on the form.
178      * @return whether the option is selected by default
179      */
180     public final boolean isDefaultSelected() {
181         return hasAttribute("selected");
182     }
183 
184     /**
185      * Returns {@code true} if the disabled attribute is set for this element. Note that this
186      * method always returns {@code false} when emulating IE, because IE does not allow individual
187      * options to be disabled.
188      *
189      * @return {@code true} if the disabled attribute is set for this element (always {@code false}
190      *         when emulating IE)
191      */
192     @Override
193     public final boolean isDisabled() {
194         if (hasFeature(HTMLOPTION_PREVENT_DISABLED)) {
195             return false;
196         }
197         return hasAttribute("disabled");
198     }
199 
200     /**
201      * {@inheritDoc}
202      */
203     @Override
204     public final String getDisabledAttribute() {
205         return getAttributeDirect("disabled");
206     }
207 
208     /**
209      * Returns the value of the attribute {@code label}. Refer to the
210      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
211      * documentation for details on the use of this attribute.
212      *
213      * @return the value of the attribute {@code label} or an empty string if that attribute isn't defined
214      */
215     public final String getLabelAttribute() {
216         return getAttributeDirect("label");
217     }
218 
219     /**
220      * Sets the value of the attribute {@code label}. Refer to the
221      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
222      * documentation for details on the use of this attribute.
223      *
224      * @param newLabel the value of the attribute {@code label}
225      */
226     public final void setLabelAttribute(final String newLabel) {
227         setAttribute("label", newLabel);
228     }
229 
230     /**
231      * Returns the value of the attribute {@code value}. Refer to the
232      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
233      * documentation for details on the use of this attribute.
234      * @see <a href="http://www.w3.org/TR/1999/REC-html401-19991224/interact/forms.html#adef-value-OPTION">
235      * initial value if value attribute is not set</a>
236      * @return the value of the attribute {@code value}
237      */
238     public final String getValueAttribute() {
239         String value = getAttributeDirect("value");
240         if (value == ATTRIBUTE_NOT_DEFINED) {
241             value = getText();
242         }
243         return value;
244     }
245 
246     /**
247      * Sets the value of the attribute {@code value}. Refer to the
248      * <a href='http://www.w3.org/TR/html401/'>HTML 4.01</a>
249      * documentation for details on the use of this attribute.
250      *
251      * @param newValue the value of the attribute {@code value}
252      */
253     public final void setValueAttribute(final String newValue) {
254         setAttribute("value", newValue);
255     }
256 
257     /**
258      * Selects the option if it's not already selected.
259      * {@inheritDoc}
260      */
261     @Override
262     public Page mouseDown(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
263         Page page = null;
264         if (hasFeature(EVENT_ONMOUSEDOWN_FOR_SELECT_OPTION_TRIGGERS_ADDITIONAL_DOWN_FOR_SELECT)) {
265             page = getEnclosingSelect().mouseDown(shiftKey, ctrlKey, altKey, button);
266         }
267         if (hasFeature(EVENT_ONMOUSEDOWN_NOT_FOR_SELECT_OPTION)) {
268             return page;
269         }
270         return super.mouseDown(shiftKey, ctrlKey, altKey, button);
271     }
272 
273     /**
274      * Selects the option if it's not already selected.
275      * {@inheritDoc}
276      */
277     @Override
278     public Page mouseUp(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
279         Page page = null;
280         if (hasFeature(EVENT_ONMOUSEUP_FOR_SELECT_OPTION_TRIGGERS_ADDITIONAL_UP_FOR_SELECT)) {
281             page = getEnclosingSelect().mouseUp(shiftKey, ctrlKey, altKey, button);
282         }
283         if (hasFeature(EVENT_ONMOUSEUP_NOT_FOR_SELECT_OPTION)) {
284             return page;
285         }
286         return super.mouseUp(shiftKey, ctrlKey, altKey, button);
287     }
288 
289     /**
290      * {@inheritDoc}
291      */
292     @Override
293     @SuppressWarnings("unchecked")
294     public <P extends Page> P click(final Event event, final boolean ignoreVisibility) throws IOException {
295         if (hasFeature(EVENT_ONCLICK_FOR_SELECT_ONLY)) {
296             final SgmlPage page = getPage();
297 
298             if (isDisabled()) {
299                 return (P) page;
300             }
301 
302             if (isStateUpdateFirst()) {
303                 doClickStateUpdate(event.isShiftKey(), event.isCtrlKey());
304             }
305 
306             return getEnclosingSelect().click(event, ignoreVisibility);
307         }
308         return super.click(event, ignoreVisibility);
309     }
310 
311     /**
312      * Selects the option if it's not already selected.
313      * {@inheritDoc}
314      */
315     @Override
316     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
317         boolean changed = false;
318         if (!isSelected()) {
319             setSelected(true, true, true, shiftKey, ctrlKey);
320             changed = true;
321         }
322         else if (getEnclosingSelect().isMultipleSelectEnabled()) {
323             if (ctrlKey) {
324                 setSelected(false, true, true, shiftKey, ctrlKey);
325                 changed = true;
326             }
327             else {
328                 getEnclosingSelect().setOnlySelected(this, true);
329             }
330         }
331         super.doClickStateUpdate(shiftKey, ctrlKey);
332         return changed;
333     }
334 
335     /**
336      * {@inheritDoc}
337      */
338     @Override
339     protected DomNode getEventTargetElement() {
340         if (hasFeature(EVENT_ONCLICK_FOR_SELECT_ONLY)) {
341             final HtmlSelect select = getEnclosingSelect();
342             if (select != null) {
343                 return select;
344             }
345         }
346         return super.getEventTargetElement();
347     }
348 
349     /**
350      * {@inheritDoc}
351      */
352     @Override
353     protected boolean isStateUpdateFirst() {
354         return true;
355     }
356 
357     /**
358      * {@inheritDoc}
359      */
360     @Override
361     protected void printOpeningTagContentAsXml(final PrintWriter printWriter) {
362         super.printOpeningTagContentAsXml(printWriter);
363         if (selected_ && getAttributeDirect("selected") == ATTRIBUTE_NOT_DEFINED) {
364             printWriter.print(" selected=\"selected\"");
365         }
366     }
367 
368     /**
369      * For internal use only.
370      * Sets/remove the selected attribute to reflect the select state
371      * @param selected the selected status
372      */
373     void setSelectedInternal(final boolean selected) {
374         selected_ = selected;
375     }
376 
377     /**
378      * {@inheritDoc}
379      * This implementation will show the label attribute before the
380      * content of the tag if the attribute exists.
381      */
382     // we need to preserve this method as it is there since many versions with the above documentation.
383     @Override
384     public String asText() {
385         return super.asText();
386     }
387 
388     /**
389      * Sets the text for this HtmlOption.
390      * @param text the text
391      */
392     public void setText(final String text) {
393         if (text == null || text.isEmpty()) {
394             removeAllChildren();
395         }
396         else {
397             final DomNode child = getFirstChild();
398             if (child == null) {
399                 appendChild(new DomText(getPage(), text));
400             }
401             else {
402                 child.setNodeValue(text);
403             }
404         }
405     }
406 
407     /**
408      * Gets the text.
409      * @return the text of this option.
410      */
411     public String getText() {
412         final HtmlSerializer ser = new HtmlSerializer();
413         ser.setIgnoreMaskedElements(false);
414         return ser.asText(this);
415     }
416 
417     /**
418      * {@inheritDoc}
419      */
420     @Override
421     public Page mouseOver(final boolean shiftKey, final boolean ctrlKey, final boolean altKey, final int button) {
422         final SgmlPage page = getPage();
423         if (page.getWebClient().getBrowserVersion().hasFeature(EVENT_ONMOUSEOVER_NEVER_FOR_SELECT_OPTION)) {
424             return page;
425         }
426         return super.mouseOver(shiftKey, ctrlKey, altKey, button);
427     }
428 
429     /**
430      * {@inheritDoc}
431      */
432     @Override
433     public DisplayStyle getDefaultStyleDisplay() {
434         if (hasFeature(CSS_DISPLAY_BLOCK2)) {
435             return DisplayStyle.BLOCK;
436         }
437         return DisplayStyle.INLINE;
438     }
439 
440     /**
441      * {@inheritDoc}
442      */
443     @Override
444     public boolean handles(final Event event) {
445         if (MouseEvent.TYPE_MOUSE_OVER.equals(event.getType())
446                 && getPage().getWebClient().getBrowserVersion().hasFeature(EVENT_ONMOUSEOVER_FOR_DISABLED_OPTION)) {
447             return true;
448         }
449         return super.handles(event);
450     }
451 }