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