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