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