View Javadoc
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.util.Map;
19  
20  import org.htmlunit.Page;
21  import org.htmlunit.ScriptResult;
22  import org.htmlunit.SgmlPage;
23  import org.htmlunit.javascript.host.event.Event;
24  
25  /**
26   * Wrapper for the HTML element "input".
27   *
28   * @author <a href="mailto:mbowler@GargoyleSoftware.com">Mike Bowler</a>
29   * @author David K. Taylor
30   * @author <a href="mailto:cse@dynabean.de">Christian Sell</a>
31   * @author Marc Guillemot
32   * @author Mike Bresnahan
33   * @author Daniel Gredler
34   * @author Bruce Faulkner
35   * @author Ahmed Ashour
36   * @author Benoit Heinrich
37   * @author Ronald Brill
38   * @author Frank Danek
39   */
40  public class HtmlRadioButtonInput extends HtmlInput implements LabelableElement {
41  
42      /**
43       * Value to use if no specified <code>value</code> attribute.
44       */
45      private static final String DEFAULT_VALUE = "on";
46  
47      private boolean defaultCheckedState_;
48      private boolean checkedState_;
49  
50      /**
51       * Creates an instance.
52       * If no value is specified, it is set to "on" as browsers do
53       * even if spec says that it is not allowed
54       * (<a href="http://www.w3.org/TR/REC-html40/interact/forms.html#adef-value-INPUT">W3C</a>).
55       *
56       * @param qualifiedName the qualified name of the element type to instantiate
57       * @param page the page that contains this element
58       * @param attributes the initial attributes
59       */
60      HtmlRadioButtonInput(final String qualifiedName, final SgmlPage page,
61              final Map<String, DomAttr> attributes) {
62          super(qualifiedName, page, attributes);
63  
64          if (getAttributeDirect(VALUE_ATTRIBUTE) == ATTRIBUTE_NOT_DEFINED) {
65              setRawValue(DEFAULT_VALUE);
66          }
67  
68          defaultCheckedState_ = hasAttribute(ATTRIBUTE_CHECKED);
69          checkedState_ = defaultCheckedState_;
70      }
71  
72      /**
73       * Returns {@code true} if this element is currently selected.
74       * @return {@code true} if this element is currently selected
75       */
76      @Override
77      public boolean isChecked() {
78          return checkedState_;
79      }
80  
81      /**
82       * {@inheritDoc}
83       * @see SubmittableElement#reset()
84       */
85      @Override
86      public void reset() {
87          setChecked(defaultCheckedState_);
88      }
89  
90      void setCheckedInternal(final boolean isChecked) {
91          checkedState_ = isChecked;
92      }
93  
94      /**
95       * Sets the {@code checked} attribute.
96       *
97       * @param isChecked true if this element is to be selected
98       * @return the page that occupies this window after setting checked status
99       *         It may be the same window or it may be a freshly loaded one.
100      */
101     @Override
102     public Page setChecked(final boolean isChecked) {
103         Page page = getPage();
104 
105         final boolean changed = isChecked() != isChecked;
106         checkedState_ = isChecked;
107         if (isChecked) {
108             final HtmlForm form = getEnclosingForm();
109             if (form != null) {
110                 form.setCheckedRadioButton(this);
111             }
112             else if (page != null && page.isHtmlPage()) {
113                 setCheckedForPage((HtmlPage) page);
114             }
115         }
116 
117         if (changed) {
118             final ScriptResult scriptResult = fireEvent(Event.TYPE_CHANGE);
119             if (scriptResult != null && page != null) {
120                 page = page.getEnclosingWindow().getWebClient().getCurrentWindow().getEnclosedPage();
121             }
122         }
123         return page;
124     }
125 
126     /**
127      * Override of default clickAction that makes this radio button the selected
128      * one when it is clicked.
129      * {@inheritDoc}
130      *
131      * @throws IOException if an IO error occurred
132      */
133     @Override
134     protected boolean doClickStateUpdate(final boolean shiftKey, final boolean ctrlKey) throws IOException {
135         final HtmlForm form = getEnclosingForm();
136         final boolean changed = !isChecked();
137 
138         final Page page = getPage();
139         if (form != null) {
140             form.setCheckedRadioButton(this);
141         }
142         else if (page != null && page.isHtmlPage()) {
143             setCheckedForPage((HtmlPage) page);
144         }
145         super.doClickStateUpdate(shiftKey, ctrlKey);
146         return changed;
147     }
148 
149     /**
150      * Select the specified radio button in the page (outside any &lt;form&gt;).
151      */
152     private void setCheckedForPage(final HtmlPage htmlPage) {
153         final String name = getNameAttribute();
154         for (final HtmlElement htmlElement : htmlPage.getHtmlElementDescendants()) {
155             if (htmlElement instanceof HtmlRadioButtonInput) {
156                 final HtmlRadioButtonInput radioInput = (HtmlRadioButtonInput) htmlElement;
157                 if (name.equals(radioInput.getAttribute(NAME_ATTRIBUTE))
158                         && radioInput.getEnclosingForm() == null) {
159                     if (radioInput == this) {
160                         setCheckedInternal(true);
161                     }
162                     else {
163                         radioInput.setCheckedInternal(false);
164                     }
165                 }
166             }
167         }
168     }
169 
170     /**
171      * {@inheritDoc}
172      */
173     @Override
174     protected void doClickFireChangeEvent() {
175         executeOnChangeHandlerIfAppropriate(this);
176     }
177 
178     /**
179      * {@inheritDoc}
180      */
181     @Override
182     protected void preventDefault() {
183         checkedState_ = !checkedState_;
184     }
185 
186     /**
187      * {@inheritDoc}
188      * Also sets the value to the new default value.
189      * @see SubmittableElement#setDefaultValue(String)
190      */
191     @Override
192     public void setDefaultValue(final String defaultValue) {
193         super.setDefaultValue(defaultValue);
194         setValue(defaultValue);
195     }
196 
197     /**
198      * {@inheritDoc}
199      * Also sets the default value.
200      */
201     @Override
202     public void setValue(final String newValue) {
203         super.setValue(newValue);
204         super.setDefaultValue(newValue);
205     }
206 
207     /**
208      * {@inheritDoc}
209      * @see SubmittableElement#setDefaultChecked(boolean)
210      */
211     @Override
212     public void setDefaultChecked(final boolean defaultChecked) {
213         defaultCheckedState_ = defaultChecked;
214         setChecked(isDefaultChecked());
215     }
216 
217     /**
218      * {@inheritDoc}
219      * @see SubmittableElement#isDefaultChecked()
220      */
221     @Override
222     public boolean isDefaultChecked() {
223         return defaultCheckedState_;
224     }
225 
226     /**
227      * {@inheritDoc}
228      */
229     @Override
230     protected boolean isStateUpdateFirst() {
231         return true;
232     }
233 
234     /**
235      * {@inheritDoc}
236      */
237     @Override
238     protected void onAddedToPage() {
239         super.onAddedToPage();
240         setChecked(isChecked());
241     }
242 
243     @Override
244     protected Object getInternalValue() {
245         return isChecked();
246     }
247 
248     @Override
249     void handleFocusLostValueChanged() {
250         // ignore
251     }
252 
253     /**
254      * {@inheritDoc}
255      */
256     @Override
257     protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue,
258             final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) {
259         final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName);
260 
261         if (VALUE_ATTRIBUTE.equals(qualifiedNameLC)) {
262             super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners,
263                     notifyMutationObservers);
264             setRawValue(attributeValue);
265             return;
266         }
267 
268         if (ATTRIBUTE_CHECKED.equals(qualifiedNameLC)) {
269             checkedState_ = true;
270         }
271         super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners,
272                 notifyMutationObservers);
273     }
274 
275     /**
276      * {@inheritDoc}
277      */
278     @Override
279     protected boolean propagateClickStateUpdateToParent() {
280         return false;
281     }
282 
283     @Override
284     public boolean isValueMissingValidityState() {
285         if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect(ATTRIBUTE_REQUIRED)) {
286             return false;
287         }
288         if (ATTRIBUTE_NOT_DEFINED == getNameAttribute()) {
289             return false;
290         }
291 
292         final String name = getNameAttribute();
293         for (final HtmlElement htmlElement : getPage().getHtmlElementDescendants()) {
294             if (htmlElement instanceof HtmlRadioButtonInput) {
295                 final HtmlRadioButtonInput radioInput = (HtmlRadioButtonInput) htmlElement;
296                 if (name.equals(radioInput.getAttribute(NAME_ATTRIBUTE))
297                         && radioInput.isChecked()) {
298                     return false;
299                 }
300             }
301         }
302         return true;
303     }
304 }