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.BrowserVersionFeatures.JS_INPUT_NUMBER_ACCEPT_ALL;
18  import static org.htmlunit.BrowserVersionFeatures.JS_INPUT_NUMBER_DOT_AT_END_IS_DOUBLE;
19  
20  import java.math.BigDecimal;
21  import java.util.Map;
22  
23  import org.htmlunit.SgmlPage;
24  import org.htmlunit.html.parser.HtmlNumberParser;
25  import org.htmlunit.util.ArrayUtils;
26  import org.htmlunit.util.StringUtils;
27  
28  /**
29   * Wrapper for the HTML element "input" with type is "number".
30   *
31   * @author Ahmed Ashour
32   * @author Ronald Brill
33   * @author Frank Danek
34   * @author Anton Demydenko
35   * @author Raik Bieniek
36   * @author Michael Lueck
37   */
38  public class HtmlNumberInput extends HtmlSelectableTextInput implements LabelableElement {
39  
40      private static final char[] VALID_INT_CHARS = "0123456789-".toCharArray();
41      private static final char[] VALID_CHARS = "0123456789-+.eE".toCharArray();
42  
43      /**
44       * Creates an instance.
45       *
46       * @param qualifiedName the qualified name of the element type to instantiate
47       * @param page the page that contains this element
48       * @param attributes the initial attributes
49       */
50      HtmlNumberInput(final String qualifiedName, final SgmlPage page,
51              final Map<String, DomAttr> attributes) {
52          super(qualifiedName, page, attributes);
53  
54          final String value = getValueAttribute();
55          if (!value.isEmpty()
56                  // Firefox is more lenient when parsing the INITIAL 'value' ATTRIBUTE specifically
57                  && !HtmlNumberParser.isValid(value, false, hasFeature(JS_INPUT_NUMBER_DOT_AT_END_IS_DOUBLE))) {
58  
59              setRawValue("");
60          }
61      }
62  
63      /**
64       * {@inheritDoc}
65       */
66      @Override
67      protected boolean isSubmittableByEnter() {
68          return true;
69      }
70  
71      /**
72       * {@inheritDoc}
73       */
74      @Override
75      public void setDefaultChecked(final boolean defaultChecked) {
76          // Empty.
77      }
78  
79      /**
80       * {@inheritDoc}
81       */
82      @Override
83      protected void doType(final char c, final boolean lastType) {
84          if (!hasFeature(JS_INPUT_NUMBER_ACCEPT_ALL) && !ArrayUtils.contains(VALID_CHARS, c)) {
85              return;
86          }
87          super.doType(c, lastType);
88      }
89  
90      /**
91       * {@inheritDoc}
92       */
93      @Override
94      public String getValue() {
95          String raw = getRawValue();
96  
97          if (StringUtils.isBlank(raw)) {
98              return "";
99          }
100 
101         if (StringUtils.equalsChar('-', raw) || StringUtils.equalsChar('+', raw)) {
102             return raw;
103         }
104 
105         // Confirmed via real-browser testing (typeIntegerWithDot): mid-typing a
106         // trailing '.' with otherwise-valid digits before it (e.g. "1.") reports
107         // the pre-dot value ("1"), not "" and not the raw "1."
108         if (raw.endsWith(".")) {
109             raw = raw.substring(0, raw.length() - 1);
110         }
111 
112         if (!HtmlNumberParser.isValid(raw, !hasFeature(JS_INPUT_NUMBER_DOT_AT_END_IS_DOUBLE), false)) {
113             return "";
114         }
115 
116         if (raw.length() > 1 && raw.charAt(0) == '+') {
117             raw = raw.substring(1, raw.length());
118         }
119 
120         return raw;
121     }
122 
123     /**
124      * {@inheritDoc}
125      */
126     @Override
127     public void setValue(final String newValue) {
128         if (StringUtils.isBlank(newValue) || !HtmlNumberParser.isValid(newValue, false, false)) {
129             super.setValue("");
130             return;
131         }
132 
133         super.setValue(newValue);
134     }
135 
136     /**
137      * Parses an attribute string (min/max/step) as a BigDecimal, or
138      * {@code null} if absent or malformed -- consolidates the several
139      * separately-inlined try/catch blocks that used to parse min/max/step
140      * individually in each constraint method.
141      */
142     private static BigDecimal parseAttributeAsBigDecimal(final String attributeValue) {
143         if (attributeValue.isEmpty()) {
144             return null;
145         }
146         try {
147             return new BigDecimal(attributeValue);
148         }
149         catch (final NumberFormatException ignored) {
150             return null;
151         }
152     }
153 
154     /**
155      * {@inheritDoc}
156      * A raw value that isn't even a well-formed number is
157      * {@link #hasBadInputValidityState()}'s concern, not a range violation.
158      */
159     @Override
160     public boolean hasRangeOverflowValidityState() {
161         if (super.hasRangeOverflowValidityState()) {
162             return true;
163         }
164 
165         final String rawValue = getRawValue();
166         if (StringUtils.isBlank(rawValue)) {
167             return false;
168         }
169 
170         final BigDecimal value = HtmlNumberParser.parse(rawValue, true, true);
171         if (value == null) {
172             return false;
173         }
174 
175         final BigDecimal max = parseAttributeAsBigDecimal(getMax());
176         return max != null && value.compareTo(max) > 0;
177     }
178 
179     /**
180      * {@inheritDoc}
181      * See {@link #hasRangeOverflowValidityState()} for the malformed-value
182      * / badInput distinction.
183      */
184     @Override
185     public boolean hasRangeUnderflowValidityState() {
186         if (super.hasRangeUnderflowValidityState()) {
187             return true;
188         }
189 
190         final String rawValue = getRawValue();
191         if (StringUtils.isBlank(rawValue)) {
192             return false;
193         }
194 
195         final BigDecimal value = HtmlNumberParser.parse(rawValue, true, true);
196         if (value == null) {
197             return false;
198         }
199 
200         final BigDecimal min = parseAttributeAsBigDecimal(getMin());
201         return min != null && value.compareTo(min) < 0;
202     }
203 
204     /**
205      * {@inheritDoc}
206      * See {@link #hasRangeOverflowValidityState()} for the malformed-value
207      * / badInput distinction.
208      */
209     @Override
210     public boolean isStepMismatchValidityState() {
211         if (super.isStepMismatchValidityState()) {
212             return true;
213         }
214 
215         final String rawValue = getRawValue();
216         if (StringUtils.isBlank(rawValue)) {
217             return false;
218         }
219 
220         final BigDecimal value = HtmlNumberParser.parse(rawValue, true, true);
221         if (value == null) {
222             return false;
223         }
224 
225         BigDecimal step = parseAttributeAsBigDecimal(getStep());
226         if (step == null) {
227             step = BigDecimal.ONE;
228         }
229 
230         BigDecimal min = parseAttributeAsBigDecimal(getMin());
231         if (min == null) {
232             min = BigDecimal.ZERO;
233         }
234 
235         return value.subtract(min).abs().remainder(step).doubleValue() > 0.0;
236     }
237 
238     /**
239      * {@inheritDoc}
240      * The new home for the malformed-value case that used to make
241      * hasRangeOverflowValidityState()/hasRangeUnderflowValidityState()/
242      * isStepMismatchValidityState() all incorrectly return {@code true}
243      * simultaneously.
244      */
245     @Override
246     public boolean hasBadInputValidityState() {
247         final String rawValue = getRawValue();
248         if (StringUtils.isBlank(rawValue)) {
249             return false;
250         }
251 
252         return !HtmlNumberParser.isValid(rawValue, !hasFeature(JS_INPUT_NUMBER_DOT_AT_END_IS_DOUBLE), true);
253     }
254 }