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