Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / form / field / Number.js
1 /**
2  * @class Ext.form.field.Number
3  * @extends Ext.form.field.Spinner
4
5 A numeric text field that provides automatic keystroke filtering to disallow non-numeric characters,
6 and numeric validation to limit the value to a range of valid numbers. The range of acceptable number
7 values can be controlled by setting the {@link #minValue} and {@link #maxValue} configs, and fractional
8 decimals can be disallowed by setting {@link #allowDecimals} to `false`.
9
10 By default, the number field is also rendered with a set of up/down spinner buttons and has
11 up/down arrow key and mouse wheel event listeners attached for incrementing/decrementing the value by the
12 {@link #step} value. To hide the spinner buttons set `{@link #hideTrigger hideTrigger}:true`; to disable the arrow key
13 and mouse wheel handlers set `{@link #keyNavEnabled keyNavEnabled}:false` and
14 `{@link #mouseWheelEnabled mouseWheelEnabled}:false`. See the example below.
15
16 #Example usage:#
17 {@img Ext.form.Number/Ext.form.Number1.png Ext.form.Number component}
18     Ext.create('Ext.form.Panel', {
19         title: 'On The Wall',
20         width: 300,
21         bodyPadding: 10,
22         renderTo: Ext.getBody(),
23         items: [{
24             xtype: 'numberfield',
25             anchor: '100%',
26             name: 'bottles',
27             fieldLabel: 'Bottles of Beer',
28             value: 99,
29             maxValue: 99,
30             minValue: 0
31         }],
32         buttons: [{
33             text: 'Take one down, pass it around',
34             handler: function() {
35                 this.up('form').down('[name=bottles]').spinDown();
36             }
37         }]
38     });
39
40 #Removing UI Enhancements#
41 {@img Ext.form.Number/Ext.form.Number2.png Ext.form.Number component}
42     Ext.create('Ext.form.Panel', {
43         title: 'Personal Info',
44         width: 300,
45         bodyPadding: 10,
46         renderTo: Ext.getBody(),        
47         items: [{
48             xtype: 'numberfield',
49             anchor: '100%',
50             name: 'age',
51             fieldLabel: 'Age',
52             minValue: 0, //prevents negative numbers
53     
54             // Remove spinner buttons, and arrow key and mouse wheel listeners
55             hideTrigger: true,
56             keyNavEnabled: false,
57             mouseWheelEnabled: false
58         }]
59     });
60
61 #Using Step#
62     Ext.create('Ext.form.Panel', {
63         renderTo: Ext.getBody(),
64         title: 'Step',
65         width: 300,
66         bodyPadding: 10,
67         items: [{
68             xtype: 'numberfield',
69             anchor: '100%',
70             name: 'evens',
71             fieldLabel: 'Even Numbers',
72
73             // Set step so it skips every other number
74             step: 2,
75             value: 0,
76
77             // Add change handler to force user-entered numbers to evens
78             listeners: {
79                 change: function(field, value) {
80                     value = parseInt(value, 10);
81                     field.setValue(value + value % 2);
82                 }
83             }
84         }]
85     });
86
87
88  * @constructor
89  * Creates a new Number field
90  * @param {Object} config Configuration options
91  *
92  * @xtype numberfield
93  * @markdown
94  * @docauthor Jason Johnston <jason@sencha.com>
95  */
96 Ext.define('Ext.form.field.Number', {
97     extend:'Ext.form.field.Spinner',
98     alias: 'widget.numberfield',
99     alternateClassName: ['Ext.form.NumberField', 'Ext.form.Number'],
100
101     /**
102      * @cfg {RegExp} stripCharsRe @hide
103      */
104     /**
105      * @cfg {RegExp} maskRe @hide
106      */
107
108     /**
109      * @cfg {Boolean} allowDecimals False to disallow decimal values (defaults to true)
110      */
111     allowDecimals : true,
112
113     /**
114      * @cfg {String} decimalSeparator Character(s) to allow as the decimal separator (defaults to '.')
115      */
116     decimalSeparator : '.',
117
118     /**
119      * @cfg {Number} decimalPrecision The maximum precision to display after the decimal separator (defaults to 2)
120      */
121     decimalPrecision : 2,
122
123     /**
124      * @cfg {Number} minValue The minimum allowed value (defaults to Number.NEGATIVE_INFINITY). Will be used by
125      * the field's validation logic, and for
126      * {@link Ext.form.field.Spinner#setSpinUpEnabled enabling/disabling the down spinner button}.
127      */
128     minValue: Number.NEGATIVE_INFINITY,
129
130     /**
131      * @cfg {Number} maxValue The maximum allowed value (defaults to Number.MAX_VALUE). Will be used by
132      * the field's validation logic, and for
133      * {@link Ext.form.field.Spinner#setSpinUpEnabled enabling/disabling the up spinner button}.
134      */
135     maxValue: Number.MAX_VALUE,
136
137     /**
138      * @cfg {Number} step Specifies a numeric interval by which the field's value will be incremented or
139      * decremented when the user invokes the spinner. Defaults to <tt>1</tt>.
140      */
141     step: 1,
142
143     /**
144      * @cfg {String} minText Error text to display if the minimum value validation fails (defaults to 'The minimum
145      * value for this field is {minValue}')
146      */
147     minText : 'The minimum value for this field is {0}',
148
149     /**
150      * @cfg {String} maxText Error text to display if the maximum value validation fails (defaults to 'The maximum
151      * value for this field is {maxValue}')
152      */
153     maxText : 'The maximum value for this field is {0}',
154
155     /**
156      * @cfg {String} nanText Error text to display if the value is not a valid number.  For example, this can happen
157      * if a valid character like '.' or '-' is left in the field with no number (defaults to '{value} is not a valid number')
158      */
159     nanText : '{0} is not a valid number',
160
161     /**
162      * @cfg {String} negativeText Error text to display if the value is negative and {@link #minValue} is set to
163      * <tt>0</tt>. This is used instead of the {@link #minText} in that circumstance only.
164      */
165     negativeText : 'The value cannot be negative',
166
167     /**
168      * @cfg {String} baseChars The base set of characters to evaluate as valid numbers (defaults to '0123456789').
169      */
170     baseChars : '0123456789',
171
172     /**
173      * @cfg {Boolean} autoStripChars True to automatically strip not allowed characters from the field. Defaults to <tt>false</tt>
174      */
175     autoStripChars: false,
176
177     initComponent: function() {
178         var me = this,
179             allowed;
180
181         this.callParent();
182
183         me.setMinValue(me.minValue);
184         me.setMaxValue(me.maxValue);
185
186         // Build regexes for masking and stripping based on the configured options
187         allowed = me.baseChars + '';
188         if (me.allowDecimals) {
189             allowed += me.decimalSeparator;
190         }
191         if (me.minValue < 0) {
192             allowed += '-';
193         }
194         allowed = Ext.String.escapeRegex(allowed);
195         me.maskRe = new RegExp('[' + allowed + ']');
196         if (me.autoStripChars) {
197             me.stripCharsRe = new RegExp('[^' + allowed + ']', 'gi');
198         }
199     },
200
201     /**
202      * Runs all of Number's validations and returns an array of any errors. Note that this first
203      * runs Text's validations, so the returned array is an amalgamation of all field errors.
204      * The additional validations run test that the value is a number, and that it is within the
205      * configured min and max values.
206      * @param {Mixed} value The value to get errors for (defaults to the current field value)
207      * @return {Array} All validation errors for this field
208      */
209     getErrors: function(value) {
210         var me = this,
211             errors = me.callParent(arguments),
212             format = Ext.String.format,
213             num;
214
215         value = Ext.isDefined(value) ? value : this.processRawValue(this.getRawValue());
216
217         if (value.length < 1) { // if it's blank and textfield didn't flag it then it's valid
218              return errors;
219         }
220
221         value = String(value).replace(me.decimalSeparator, '.');
222
223         if(isNaN(value)){
224             errors.push(format(me.nanText, value));
225         }
226
227         num = me.parseValue(value);
228
229         if (me.minValue === 0 && num < 0) {
230             errors.push(this.negativeText);
231         }
232         else if (num < me.minValue) {
233             errors.push(format(me.minText, me.minValue));
234         }
235
236         if (num > me.maxValue) {
237             errors.push(format(me.maxText, me.maxValue));
238         }
239
240
241         return errors;
242     },
243
244     rawToValue: function(rawValue) {
245         return this.fixPrecision(this.parseValue(rawValue)) || rawValue || null;
246     },
247
248     valueToRaw: function(value) {
249         var me = this,
250             decimalSeparator = me.decimalSeparator;
251         value = me.parseValue(value);
252         value = me.fixPrecision(value);
253         value = Ext.isNumber(value) ? value : parseFloat(String(value).replace(decimalSeparator, '.'));
254         value = isNaN(value) ? '' : String(value).replace('.', decimalSeparator);
255         return value;
256     },
257
258     onChange: function() {
259         var me = this,
260             value = me.getValue(),
261             valueIsNull = value === null;
262
263         me.callParent(arguments);
264
265         // Update the spinner buttons
266         me.setSpinUpEnabled(valueIsNull || value < me.maxValue);
267         me.setSpinDownEnabled(valueIsNull || value > me.minValue);
268     },
269
270     /**
271      * Replaces any existing {@link #minValue} with the new value.
272      * @param {Number} value The minimum value
273      */
274     setMinValue : function(value) {
275         this.minValue = Ext.Number.from(value, Number.NEGATIVE_INFINITY);
276     },
277
278     /**
279      * Replaces any existing {@link #maxValue} with the new value.
280      * @param {Number} value The maximum value
281      */
282     setMaxValue: function(value) {
283         this.maxValue = Ext.Number.from(value, Number.MAX_VALUE);
284     },
285
286     // private
287     parseValue : function(value) {
288         value = parseFloat(String(value).replace(this.decimalSeparator, '.'));
289         return isNaN(value) ? null : value;
290     },
291
292     /**
293      * @private
294      *
295      */
296     fixPrecision : function(value) {
297         var me = this,
298             nan = isNaN(value),
299             precision = me.decimalPrecision;
300
301         if (nan || !value) {
302             return nan ? '' : value;
303         } else if (!me.allowDecimals || precision <= 0) {
304             precision = 0;
305         }
306
307         return parseFloat(Ext.Number.toFixed(parseFloat(value), precision));
308     },
309
310     beforeBlur : function() {
311         var me = this,
312             v = me.parseValue(me.getRawValue());
313
314         if (!Ext.isEmpty(v)) {
315             me.setValue(v);
316         }
317     },
318
319     onSpinUp: function() {
320         var me = this;
321         if (!me.readOnly) {
322             me.setValue(Ext.Number.constrain(me.getValue() + me.step, me.minValue, me.maxValue));
323         }
324     },
325
326     onSpinDown: function() {
327         var me = this;
328         if (!me.readOnly) {
329             me.setValue(Ext.Number.constrain(me.getValue() - me.step, me.minValue, me.maxValue));
330         }
331     }
332 });