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