View Javadoc
1   /*
2    * Copyright (c) 2002-2026 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 Mike Bowler
29   * @author David K. Taylor
30   * @author Christian Sell
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 radioInput) {
156                 if (name.equals(radioInput.getAttribute(NAME_ATTRIBUTE))
157                         && radioInput.getEnclosingForm() == null) {
158                     if (radioInput == this) {
159                         setCheckedInternal(true);
160                     }
161                     else {
162                         radioInput.setCheckedInternal(false);
163                     }
164                 }
165             }
166         }
167     }
168 
169     /**
170      * {@inheritDoc}
171      */
172     @Override
173     protected void doClickFireChangeEvent() {
174         executeOnChangeHandlerIfAppropriate(this);
175     }
176 
177     /**
178      * {@inheritDoc}
179      */
180     @Override
181     protected void preventDefault() {
182         checkedState_ = !checkedState_;
183     }
184 
185     /**
186      * {@inheritDoc}
187      * Also sets the value to the new default value.
188      * @see SubmittableElement#setDefaultValue(String)
189      */
190     @Override
191     public void setDefaultValue(final String defaultValue) {
192         super.setDefaultValue(defaultValue);
193         setValue(defaultValue);
194     }
195 
196     /**
197      * {@inheritDoc}
198      * Also sets the default value.
199      */
200     @Override
201     public void setValue(final String newValue) {
202         super.setValue(newValue);
203         super.setDefaultValue(newValue);
204     }
205 
206     /**
207      * {@inheritDoc}
208      * @see SubmittableElement#setDefaultChecked(boolean)
209      */
210     @Override
211     public void setDefaultChecked(final boolean defaultChecked) {
212         defaultCheckedState_ = defaultChecked;
213         setChecked(isDefaultChecked());
214     }
215 
216     /**
217      * {@inheritDoc}
218      * @see SubmittableElement#isDefaultChecked()
219      */
220     @Override
221     public boolean isDefaultChecked() {
222         return defaultCheckedState_;
223     }
224 
225     /**
226      * {@inheritDoc}
227      */
228     @Override
229     protected boolean isStateUpdateFirst() {
230         return true;
231     }
232 
233     /**
234      * {@inheritDoc}
235      */
236     @Override
237     protected void onAddedToPage() {
238         super.onAddedToPage();
239         setChecked(isChecked());
240     }
241 
242     @Override
243     protected Object getInternalValue() {
244         return isChecked();
245     }
246 
247     @Override
248     void handleFocusLostValueChanged() {
249         // ignore
250     }
251 
252     /**
253      * {@inheritDoc}
254      */
255     @Override
256     protected void setAttributeNS(final String namespaceURI, final String qualifiedName, final String attributeValue,
257             final boolean notifyAttributeChangeListeners, final boolean notifyMutationObservers) {
258         final String qualifiedNameLC = org.htmlunit.util.StringUtils.toRootLowerCase(qualifiedName);
259 
260         if (VALUE_ATTRIBUTE.equals(qualifiedNameLC)) {
261             super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners,
262                     notifyMutationObservers);
263             setRawValue(attributeValue);
264             return;
265         }
266 
267         if (ATTRIBUTE_CHECKED.equals(qualifiedNameLC)) {
268             checkedState_ = true;
269         }
270         super.setAttributeNS(namespaceURI, qualifiedNameLC, attributeValue, notifyAttributeChangeListeners,
271                 notifyMutationObservers);
272     }
273 
274     /**
275      * {@inheritDoc}
276      */
277     @Override
278     protected boolean propagateClickStateUpdateToParent() {
279         return false;
280     }
281 
282     @Override
283     public boolean isValueMissingValidityState() {
284         if (ATTRIBUTE_NOT_DEFINED == getAttributeDirect(ATTRIBUTE_REQUIRED)) {
285             return false;
286         }
287         if (ATTRIBUTE_NOT_DEFINED == getNameAttribute()) {
288             return false;
289         }
290 
291         final String name = getNameAttribute();
292         for (final HtmlElement htmlElement : getPage().getHtmlElementDescendants()) {
293             if (htmlElement instanceof HtmlRadioButtonInput radioInput) {
294                 if (name.equals(radioInput.getAttribute(NAME_ATTRIBUTE))
295                         && radioInput.isChecked()) {
296                     return false;
297                 }
298             }
299         }
300         return true;
301     }
302 }