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