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 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.text.NumberFormat;
22  import java.text.ParseException;
23  import java.util.Locale;
24  import java.util.Map;
25  
26  import org.htmlunit.SgmlPage;
27  import org.htmlunit.util.ArrayUtils;
28  import org.htmlunit.util.StringUtils;
29  
30  /**
31   * Wrapper for the HTML element "input" with type is "number".
32   *
33   * @author Ahmed Ashour
34   * @author Ronald Brill
35   * @author Frank Danek
36   * @author Anton Demydenko
37   * @author Raik Bieniek
38   * @author Michael Lueck
39   */
40  public class HtmlNumberInput extends HtmlSelectableTextInput implements LabelableElement {
41  
42      private static final char[] VALID_INT_CHARS = "0123456789-".toCharArray();
43      private static final char[] VALID_CHARS = "0123456789-+.eE".toCharArray();
44  
45      /**
46       * Creates an instance.
47       *
48       * @param qualifiedName the qualified name of the element type to instantiate
49       * @param page the page that contains this element
50       * @param attributes the initial attributes
51       */
52      HtmlNumberInput(final String qualifiedName, final SgmlPage page,
53              final Map<String, DomAttr> attributes) {
54          super(qualifiedName, page, attributes);
55  
56          final String value = getValueAttribute();
57          if (!value.isEmpty()) {
58              if (!StringUtils.containsOnly(value, VALID_CHARS)) {
59                  setRawValue("");
60              }
61          }
62      }
63  
64      /**
65       * {@inheritDoc}
66       */
67      @Override
68      protected boolean isSubmittableByEnter() {
69          return true;
70      }
71  
72      /**
73       * {@inheritDoc}
74       */
75      @Override
76      public void setDefaultChecked(final boolean defaultChecked) {
77          // Empty.
78      }
79  
80      /**
81       * {@inheritDoc}
82       */
83      @Override
84      protected void doType(final char c, final boolean lastType) {
85          if (!hasFeature(JS_INPUT_NUMBER_ACCEPT_ALL)) {
86              if (!ArrayUtils.contains(VALID_CHARS, c)) {
87                  return;
88              }
89          }
90  
91          super.doType(c, lastType);
92      }
93  
94      /**
95       * {@inheritDoc}
96       */
97      @Override
98      public String getValue() {
99          final String raw = getRawValue();
100 
101         if (org.htmlunit.util.StringUtils.isBlank(raw)) {
102             return "";
103         }
104 
105         if (org.htmlunit.util.StringUtils.equalsChar('-', raw)
106                 || org.htmlunit.util.StringUtils.equalsChar('+', raw)) {
107             return raw;
108         }
109 
110         try {
111             final String lang = getPage().getWebClient().getBrowserVersion().getBrowserLanguage();
112             final NumberFormat format = NumberFormat.getInstance(Locale.forLanguageTag(lang));
113             format.parse(raw);
114 
115             return raw.trim();
116         }
117         catch (final ParseException ignored) {
118             // ignore
119         }
120 
121         if (hasFeature(JS_INPUT_NUMBER_ACCEPT_ALL)) {
122             return raw;
123         }
124 
125         return "";
126     }
127 
128     /**
129      * {@inheritDoc}
130      */
131     @Override
132     public boolean isValid() {
133         if (!super.isValid()) {
134             return false;
135         }
136 
137         String rawValue = getRawValue();
138         if (org.htmlunit.util.StringUtils.isBlank(rawValue)) {
139             return true;
140         }
141 
142         if (!hasFeature(JS_INPUT_NUMBER_ACCEPT_ALL)) {
143             rawValue = rawValue.replaceAll("\\s", "");
144         }
145         if (!rawValue.isEmpty()) {
146             if (org.htmlunit.util.StringUtils.equalsChar('-', rawValue)
147                     || org.htmlunit.util.StringUtils.equalsChar('+', rawValue)) {
148                 return false;
149             }
150 
151             // if we have no step, the value has to be an integer
152             if (getStep().isEmpty()) {
153                 String val = rawValue;
154                 final int lastPos = val.length() - 1;
155                 if (lastPos >= 0 && val.charAt(lastPos) == '.') {
156                     if (hasFeature(JS_INPUT_NUMBER_DOT_AT_END_IS_DOUBLE)) {
157                         return false;
158                     }
159                     val = val.substring(0, lastPos);
160                 }
161                 if (!StringUtils.containsOnly(val, VALID_INT_CHARS)) {
162                     return false;
163                 }
164             }
165 
166             final BigDecimal value;
167             try {
168                 value = new BigDecimal(rawValue);
169             }
170             catch (final NumberFormatException e) {
171                 return false;
172             }
173 
174             if (!getMin().isEmpty()) {
175                 try {
176                     final BigDecimal min = new BigDecimal(getMin());
177                     if (value.compareTo(min) < 0) {
178                         return false;
179                     }
180 
181                     if (!getStep().isEmpty()) {
182                         try {
183                             final BigDecimal step = new BigDecimal(getStep());
184                             if (value.subtract(min).abs().remainder(step).doubleValue() > 0.0) {
185                                 return false;
186                             }
187                         }
188                         catch (final NumberFormatException ignored) {
189                             // ignore
190                         }
191                     }
192                 }
193                 catch (final NumberFormatException ignored) {
194                     // ignore
195                 }
196             }
197             if (!getMax().isEmpty()) {
198                 try {
199                     final BigDecimal max = new BigDecimal(getMax());
200                     if (value.compareTo(max) > 0) {
201                         return false;
202                     }
203                 }
204                 catch (final NumberFormatException ignored) {
205                     // ignore
206                 }
207             }
208         }
209         return true;
210     }
211 }