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.EVENT_ONCHANGE_AFTER_ONCLICK;
18  import static com.gargoylesoftware.htmlunit.BrowserVersionFeatures.HTMLINPUT_CHECKBOX_DOES_NOT_CLICK_SURROUNDING_ANCHOR;
19  
20  import java.io.IOException;
21  import java.util.List;
22  import java.util.Map;
23  
24  import com.gargoylesoftware.htmlunit.Page;
25  import com.gargoylesoftware.htmlunit.ScriptResult;
26  import com.gargoylesoftware.htmlunit.SgmlPage;
27  import com.gargoylesoftware.htmlunit.javascript.host.event.Event;
28  
29  /**
30   * Wrapper for the HTML element "input".
31   *
32   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
33   * @author David K. Taylor
34   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
35   * @author Marc Guillemot
36   * @author Mike Bresnahan
37   * @author Daniel Gredler
38   * @author Bruce Faulkner
39   * @author Ahmed Ashour
40   * @author Benoit Heinrich
41   * @author Ronald Brill
42   * @author Frank Danek
43   */
44  public class HtmlRadioButtonInput extends HtmlInput {
45  
46      /**
47       * Value to use if no specified <tt>value</tt> attribute.
48       */
49      private static final String DEFAULT_VALUE = "on";
50  
51      private boolean defaultCheckedState_;
52      private boolean checkedState_;
53  
54      /**
55       * Creates an instance.
56       * If no value is specified, it is set to "on" as browsers do (eg IE6 and Mozilla 1.7)
57       * even if spec says that it is not allowed
58       * (<a href="http://www.w3.org/TR/REC-html40/interact/forms.html#adef-value-INPUT">W3C</a>).
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      HtmlRadioButtonInput(final String qualifiedName, final SgmlPage page,
65              final Map<String, DomAttr> attributes) {
66          // default value for both IE6 and Mozilla 1.7 even if spec says it is unspecified
67          super(qualifiedName, page, addValueIfNeeded(page, attributes));
68  
69          // fix the default value in case we have set it
70          if (getAttribute("value") == DEFAULT_VALUE) {
71              setDefaultValue(ATTRIBUTE_NOT_DEFINED, false);
72          }
73  
74          defaultCheckedState_ = hasAttribute("checked");
75          checkedState_ = defaultCheckedState_;
76      }
77  
78      /**
79       * Add missing attribute if needed by fixing attribute map rather to add it afterwards as this second option
80       * triggers the instantiation of the script object at a time where the DOM node has not yet been added to its
81       * parent.
82       */
83      private static Map<String, DomAttr> addValueIfNeeded(final SgmlPage page,
84              final Map<String, DomAttr> attributes) {
85  
86          for (final String key : attributes.keySet()) {
87              if ("value".equalsIgnoreCase(key)) {
88                  return attributes; // value attribute was specified
89              }
90          }
91  
92          // value attribute was not specified, add it
93          final DomAttr newAttr = new DomAttr(page, null, "value", DEFAULT_VALUE, true);
94          attributes.put("value", newAttr);
95  
96          return attributes;
97      }
98  
99      /**
100      * Returns {@code true} if this element is currently selected.
101      * @return {@code true} if this element is currently selected
102      */
103     @Override
104     public boolean isChecked() {
105         return checkedState_;
106     }
107 
108     /**
109      * {@inheritDoc}
110      * @see SubmittableElement#reset()
111      */
112     @Override
113     public void reset() {
114         setChecked(defaultCheckedState_);
115     }
116 
117     void setCheckedInternal(final boolean isChecked) {
118         checkedState_ = isChecked;
119     }
120 
121     /**
122      * Sets the {@code checked} attribute.
123      *
124      * @param isChecked true if this element is to be selected
125      * @return the page that occupies this window after setting checked status
126      * It may be the same window or it may be a freshly loaded one.
127      */
128     @Override
129     public Page setChecked(final boolean isChecked) {
130         Page page = getPage();
131 
132         final boolean changed = isChecked() != isChecked;
133         checkedState_ = isChecked;
134         if (isChecked) {
135             final HtmlForm form = getEnclosingForm();
136             if (form != null) {
137                 form.setCheckedRadioButton(this);
138             }
139             else if (page != null && page.isHtmlPage()) {
140                 setCheckedForPage((HtmlPage) page);
141             }
142         }
143 
144         if (changed) {
145             final ScriptResult scriptResult = fireEvent(Event.TYPE_CHANGE);
146             if (scriptResult != null) {
147                 page = scriptResult.getNewPage();
148             }
149         }
150         return page;
151     }
152 
153     /**
154      * Override of default clickAction that makes this radio button the selected
155      * one when it is clicked.
156      * {@inheritDoc}
157      *
158      * @throws IOException if an IO error occurred
159      */
160     @Override
161     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
162         final HtmlForm form = getEnclosingForm();
163         final boolean changed = !isChecked();
164 
165         final Page page = getPage();
166         if (form != null) {
167             form.setCheckedRadioButton(this);
168         }
169         else if (page != null && page.isHtmlPage()) {
170             setCheckedForPage((HtmlPage) page);
171         }
172         super.doClickStateUpdate(shiftKey, ctrlKey);
173         return changed;
174     }
175 
176     /**
177      * Select the specified radio button in the page (outside any &lt;form&gt;).
178      *
179      * @param radioButtonInput the radio Button
180      */
181     private void setCheckedForPage(final HtmlPage htmlPage) {
182         // May be done in single XPath search?
183         final List<HtmlRadioButtonInput> pageInputs =
184             htmlPage.getByXPath("//input[lower-case(@type)='radio' "
185                 + "and @name='" + getNameAttribute() + "']");
186         final List<HtmlRadioButtonInput> formInputs =
187             htmlPage.getByXPath("//form//input[lower-case(@type)='radio' "
188                 + "and @name='" + getNameAttribute() + "']");
189 
190         pageInputs.removeAll(formInputs);
191 
192         boolean foundInPage = false;
193         for (final HtmlRadioButtonInput input : pageInputs) {
194             if (input == this) {
195                 setCheckedInternal(true);
196                 foundInPage = true;
197             }
198             else {
199                 input.setCheckedInternal(false);
200             }
201         }
202 
203         if (!foundInPage && !formInputs.contains(this)) {
204             setCheckedInternal(true);
205         }
206     }
207 
208     /**
209      * {@inheritDoc}
210      */
211     @Override
212     protected ScriptResult doClickFireClickEvent(final Event event) {
213         if (!hasFeature(EVENT_ONCHANGE_AFTER_ONCLICK)) {
214             executeOnChangeHandlerIfAppropriate(this);
215         }
216 
217         return super.doClickFireClickEvent(event);
218     }
219 
220     /**
221      * {@inheritDoc}
222      */
223     @Override
224     protected void doClickFireChangeEvent() {
225         if (hasFeature(EVENT_ONCHANGE_AFTER_ONCLICK)) {
226             executeOnChangeHandlerIfAppropriate(this);
227         }
228     }
229 
230     /**
231      * A radio button does not have a textual representation,
232      * but we invent one for it because it is useful for testing.
233      * @return "checked" or "unchecked" according to the radio state
234      */
235     // we need to preserve this method as it is there since many versions with the above documentation.
236     @Override
237     public String asText() {
238         return super.asText();
239     }
240 
241     /**
242      * {@inheritDoc}
243      */
244     @Override
245     protected void preventDefault() {
246         checkedState_ = !checkedState_;
247     }
248 
249     /**
250      * {@inheritDoc}
251      * Also sets the value to the new default value.
252      * @see SubmittableElement#setDefaultValue(String)
253      */
254     @Override
255     public void setDefaultValue(final String defaultValue) {
256         super.setDefaultValue(defaultValue);
257         setValueAttribute(defaultValue);
258     }
259 
260     /**
261      * {@inheritDoc}
262      * @see SubmittableElement#setDefaultChecked(boolean)
263      */
264     @Override
265     public void setDefaultChecked(final boolean defaultChecked) {
266         defaultCheckedState_ = defaultChecked;
267         setChecked(isDefaultChecked());
268     }
269 
270     /**
271      * {@inheritDoc}
272      * @see SubmittableElement#isDefaultChecked()
273      */
274     @Override
275     public boolean isDefaultChecked() {
276         return defaultCheckedState_;
277     }
278 
279     /**
280      * {@inheritDoc}
281      */
282     @Override
283     protected boolean isStateUpdateFirst() {
284         return true;
285     }
286 
287     /**
288      * {@inheritDoc}
289      */
290     @Override
291     protected void onAddedToPage() {
292         super.onAddedToPage();
293         setChecked(isChecked());
294     }
295 
296     @Override
297     Object getInternalValue() {
298         return isChecked();
299     }
300 
301     @Override
302     void handleFocusLostValueChanged() {
303     }
304 
305     /**
306      * {@inheritDoc}
307      */
308     @Override
309     protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue,
310             final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) {
311         if ("value".equals(qualifiedName)) {
312             setDefaultValue(attributeValue, false);
313         }
314         if ("checked".equals(qualifiedName)) {
315             checkedState_ = true;
316         }
317         super.setAttributeNS(namespaceURI, qualifiedName, attributeValue, notifyAttributeChangeListeners,
318                 notifyMutationObservers);
319     }
320 
321     /**
322      * {@inheritDoc}
323      */
324     @Override
325     protected boolean propagateClickStateUpdateToParent() {
326         return !hasFeature(HTMLINPUT_CHECKBOX_DOES_NOT_CLICK_SURROUNDING_ANCHOR)
327                 && super.propagateClickStateUpdateToParent();
328     }
329 
330 }