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