Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / form / field / Text.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 basic text field.  Can be used as a direct replacement for traditional text inputs,
19  * or as the base class for more sophisticated input controls (like {@link Ext.form.field.TextArea}
20  * and {@link Ext.form.field.ComboBox}). Has support for empty-field placeholder values (see {@link #emptyText}).
21  *
22  * # Validation
23  *
24  * The Text field has a useful set of validations built in:
25  *
26  * - {@link #allowBlank} for making the field required
27  * - {@link #minLength} for requiring a minimum value length
28  * - {@link #maxLength} for setting a maximum value length (with {@link #enforceMaxLength} to add it
29  *   as the `maxlength` attribute on the input element)
30  * - {@link #regex} to specify a custom regular expression for validation
31  *
32  * In addition, custom validations may be added:
33  *
34  * - {@link #vtype} specifies a virtual type implementation from {@link Ext.form.field.VTypes} which can contain
35  *   custom validation logic
36  * - {@link #validator} allows a custom arbitrary function to be called during validation
37  *
38  * The details around how and when each of these validation options get used are described in the
39  * documentation for {@link #getErrors}.
40  *
41  * By default, the field value is checked for validity immediately while the user is typing in the
42  * field. This can be controlled with the {@link #validateOnChange}, {@link #checkChangeEvents}, and
43  * {@link #checkChangeBuffer} configurations. Also see the details on Form Validation in the
44  * {@link Ext.form.Panel} class documentation.
45  *
46  * # Masking and Character Stripping
47  *
48  * Text fields can be configured with custom regular expressions to be applied to entered values before
49  * validation: see {@link #maskRe} and {@link #stripCharsRe} for details.
50  *
51  * # Example usage
52  *
53  *     @example
54  *     Ext.create('Ext.form.Panel', {
55  *         title: 'Contact Info',
56  *         width: 300,
57  *         bodyPadding: 10,
58  *         renderTo: Ext.getBody(),
59  *         items: [{
60  *             xtype: 'textfield',
61  *             name: 'name',
62  *             fieldLabel: 'Name',
63  *             allowBlank: false  // requires a non-empty value
64  *         }, {
65  *             xtype: 'textfield',
66  *             name: 'email',
67  *             fieldLabel: 'Email Address',
68  *             vtype: 'email'  // requires value to be a valid email address format
69  *         }]
70  *     });
71  */
72 Ext.define('Ext.form.field.Text', {
73     extend:'Ext.form.field.Base',
74     alias: 'widget.textfield',
75     requires: ['Ext.form.field.VTypes', 'Ext.layout.component.field.Text'],
76     alternateClassName: ['Ext.form.TextField', 'Ext.form.Text'],
77
78     /**
79      * @cfg {String} vtypeText
80      * A custom error message to display in place of the default message provided for the **`{@link #vtype}`** currently
81      * set for this field. **Note**: only applies if **`{@link #vtype}`** is set, else ignored.
82      */
83
84     /**
85      * @cfg {RegExp} stripCharsRe
86      * A JavaScript RegExp object used to strip unwanted content from the value
87      * before validation. If <tt>stripCharsRe</tt> is specified,
88      * every character matching <tt>stripCharsRe</tt> will be removed before fed to validation.
89      * This does not change the value of the field.
90      */
91
92     /**
93      * @cfg {Number} size
94      * An initial value for the 'size' attribute on the text input element. This is only used if the field has no
95      * configured {@link #width} and is not given a width by its container's layout. Defaults to 20.
96      */
97     size: 20,
98
99     /**
100      * @cfg {Boolean} [grow=false]
101      * true if this field should automatically grow and shrink to its content
102      */
103
104     /**
105      * @cfg {Number} growMin
106      * The minimum width to allow when `{@link #grow} = true`
107      */
108     growMin : 30,
109
110     /**
111      * @cfg {Number} growMax
112      * The maximum width to allow when `{@link #grow} = true`
113      */
114     growMax : 800,
115
116     /**
117      * @cfg {String} growAppend
118      * A string that will be appended to the field's current value for the purposes of calculating the target field
119      * size. Only used when the {@link #grow} config is true. Defaults to a single capital "W" (the widest character in
120      * common fonts) to leave enough space for the next typed character and avoid the field value shifting before the
121      * width is adjusted.
122      */
123     growAppend: 'W',
124
125     /**
126      * @cfg {String} vtype
127      * A validation type name as defined in {@link Ext.form.field.VTypes}
128      */
129
130     /**
131      * @cfg {RegExp} maskRe An input mask regular expression that will be used to filter keystrokes (character being
132      * typed) that do not match.
133      * Note: It dose not filter characters already in the input.
134      */
135
136     /**
137      * @cfg {Boolean} [disableKeyFilter=false]
138      * Specify true to disable input keystroke filtering
139      */
140
141     /**
142      * @cfg {Boolean} allowBlank
143      * Specify false to validate that the value's length is > 0
144      */
145     allowBlank : true,
146
147     /**
148      * @cfg {Number} minLength
149      * Minimum input field length required
150      */
151     minLength : 0,
152
153     /**
154      * @cfg {Number} maxLength
155      * Maximum input field length allowed by validation (defaults to Number.MAX_VALUE). This behavior is intended to
156      * provide instant feedback to the user by improving usability to allow pasting and editing or overtyping and back
157      * tracking. To restrict the maximum number of characters that can be entered into the field use the **{@link
158      * Ext.form.field.Text#enforceMaxLength enforceMaxLength}** option.
159      */
160     maxLength : Number.MAX_VALUE,
161
162     /**
163      * @cfg {Boolean} enforceMaxLength
164      * True to set the maxLength property on the underlying input field. Defaults to false
165      */
166
167     /**
168      * @cfg {String} minLengthText
169      * Error text to display if the **{@link #minLength minimum length}** validation fails.
170      */
171     minLengthText : 'The minimum length for this field is {0}',
172
173     /**
174      * @cfg {String} maxLengthText
175      * Error text to display if the **{@link #maxLength maximum length}** validation fails
176      */
177     maxLengthText : 'The maximum length for this field is {0}',
178
179     /**
180      * @cfg {Boolean} [selectOnFocus=false]
181      * true to automatically select any existing field text when the field receives input focus
182      */
183
184     /**
185      * @cfg {String} blankText
186      * The error text to display if the **{@link #allowBlank}** validation fails
187      */
188     blankText : 'This field is required',
189
190     /**
191      * @cfg {Function} validator
192      * A custom validation function to be called during field validation ({@link #getErrors}).
193      * If specified, this function will be called first, allowing the developer to override the default validation
194      * process.
195      *
196      * This function will be passed the following parameters:
197      *
198      * @cfg {Object} validator.value The current field value
199      * @cfg {Boolean/String} validator.return
200      *
201      * - True if the value is valid
202      * - An error message if the value is invalid
203      */
204
205     /**
206      * @cfg {RegExp} regex A JavaScript RegExp object to be tested against the field value during validation.
207      * If the test fails, the field will be marked invalid using
208      * either <b><tt>{@link #regexText}</tt></b> or <b><tt>{@link #invalidText}</tt></b>.
209      */
210
211     /**
212      * @cfg {String} regexText
213      * The error text to display if **{@link #regex}** is used and the test fails during validation
214      */
215     regexText : '',
216
217     /**
218      * @cfg {String} emptyText
219      * The default text to place into an empty field.
220      *
221      * Note that normally this value will be submitted to the server if this field is enabled; to prevent this you can
222      * set the {@link Ext.form.action.Action#submitEmptyText submitEmptyText} option of {@link Ext.form.Basic#submit} to
223      * false.
224      *
225      * Also note that if you use {@link #inputType inputType}:'file', {@link #emptyText} is not supported and should be
226      * avoided.
227      */
228
229     /**
230      * @cfg {String} [emptyCls='x-form-empty-field']
231      * The CSS class to apply to an empty field to style the **{@link #emptyText}**.
232      * This class is automatically added and removed as needed depending on the current field value.
233      */
234     emptyCls : Ext.baseCSSPrefix + 'form-empty-field',
235
236     ariaRole: 'textbox',
237
238     /**
239      * @cfg {Boolean} [enableKeyEvents=false]
240      * true to enable the proxying of key events for the HTML input field
241      */
242
243     componentLayout: 'textfield',
244
245     initComponent : function(){
246         this.callParent();
247         this.addEvents(
248             /**
249              * @event autosize
250              * Fires when the **{@link #autoSize}** function is triggered and the field is resized according to the
251              * {@link #grow}/{@link #growMin}/{@link #growMax} configs as a result. This event provides a hook for the
252              * developer to apply additional logic at runtime to resize the field if needed.
253              * @param {Ext.form.field.Text} this This text field
254              * @param {Number} width The new field width
255              */
256             'autosize',
257
258             /**
259              * @event keydown
260              * Keydown input field event. This event only fires if **{@link #enableKeyEvents}** is set to true.
261              * @param {Ext.form.field.Text} this This text field
262              * @param {Ext.EventObject} e
263              */
264             'keydown',
265             /**
266              * @event keyup
267              * Keyup input field event. This event only fires if **{@link #enableKeyEvents}** is set to true.
268              * @param {Ext.form.field.Text} this This text field
269              * @param {Ext.EventObject} e
270              */
271             'keyup',
272             /**
273              * @event keypress
274              * Keypress input field event. This event only fires if **{@link #enableKeyEvents}** is set to true.
275              * @param {Ext.form.field.Text} this This text field
276              * @param {Ext.EventObject} e
277              */
278             'keypress'
279         );
280     },
281
282     // private
283     initEvents : function(){
284         var me = this,
285             el = me.inputEl;
286
287         me.callParent();
288         if(me.selectOnFocus || me.emptyText){
289             me.mon(el, 'mousedown', me.onMouseDown, me);
290         }
291         if(me.maskRe || (me.vtype && me.disableKeyFilter !== true && (me.maskRe = Ext.form.field.VTypes[me.vtype+'Mask']))){
292             me.mon(el, 'keypress', me.filterKeys, me);
293         }
294
295         if (me.enableKeyEvents) {
296             me.mon(el, {
297                 scope: me,
298                 keyup: me.onKeyUp,
299                 keydown: me.onKeyDown,
300                 keypress: me.onKeyPress
301             });
302         }
303     },
304
305     /**
306      * @private
307      * Override. Treat undefined and null values as equal to an empty string value.
308      */
309     isEqual: function(value1, value2) {
310         return this.isEqualAsString(value1, value2);
311     },
312
313     /**
314      * @private
315      * If grow=true, invoke the autoSize method when the field's value is changed.
316      */
317     onChange: function() {
318         this.callParent();
319         this.autoSize();
320     },
321
322     afterRender: function(){
323         var me = this;
324         if (me.enforceMaxLength) {
325             me.inputEl.dom.maxLength = me.maxLength;
326         }
327         me.applyEmptyText();
328         me.autoSize();
329         me.callParent();
330     },
331
332     onMouseDown: function(e){
333         var me = this;
334         if(!me.hasFocus){
335             me.mon(me.inputEl, 'mouseup', Ext.emptyFn, me, { single: true, preventDefault: true });
336         }
337     },
338
339     /**
340      * Performs any necessary manipulation of a raw String value to prepare it for conversion and/or
341      * {@link #validate validation}. For text fields this applies the configured {@link #stripCharsRe}
342      * to the raw value.
343      * @param {String} value The unprocessed string value
344      * @return {String} The processed string value
345      */
346     processRawValue: function(value) {
347         var me = this,
348             stripRe = me.stripCharsRe,
349             newValue;
350
351         if (stripRe) {
352             newValue = value.replace(stripRe, '');
353             if (newValue !== value) {
354                 me.setRawValue(newValue);
355                 value = newValue;
356             }
357         }
358         return value;
359     },
360
361     //private
362     onDisable: function(){
363         this.callParent();
364         if (Ext.isIE) {
365             this.inputEl.dom.unselectable = 'on';
366         }
367     },
368
369     //private
370     onEnable: function(){
371         this.callParent();
372         if (Ext.isIE) {
373             this.inputEl.dom.unselectable = '';
374         }
375     },
376
377     onKeyDown: function(e) {
378         this.fireEvent('keydown', this, e);
379     },
380
381     onKeyUp: function(e) {
382         this.fireEvent('keyup', this, e);
383     },
384
385     onKeyPress: function(e) {
386         this.fireEvent('keypress', this, e);
387     },
388
389     /**
390      * Resets the current field value to the originally-loaded value and clears any validation messages.
391      * Also adds **{@link #emptyText}** and **{@link #emptyCls}** if the original value was blank.
392      */
393     reset : function(){
394         this.callParent();
395         this.applyEmptyText();
396     },
397
398     applyEmptyText : function(){
399         var me = this,
400             emptyText = me.emptyText,
401             isEmpty;
402
403         if (me.rendered && emptyText) {
404             isEmpty = me.getRawValue().length < 1 && !me.hasFocus;
405
406             if (Ext.supports.Placeholder) {
407                 me.inputEl.dom.placeholder = emptyText;
408             } else if (isEmpty) {
409                 me.setRawValue(emptyText);
410             }
411
412             //all browsers need this because of a styling issue with chrome + placeholders.
413             //the text isnt vertically aligned when empty (and using the placeholder)
414             if (isEmpty) {
415                 me.inputEl.addCls(me.emptyCls);
416             }
417
418             me.autoSize();
419         }
420     },
421
422     // private
423     preFocus : function(){
424         var me = this,
425             inputEl = me.inputEl,
426             emptyText = me.emptyText,
427             isEmpty;
428
429         if (emptyText && !Ext.supports.Placeholder && inputEl.dom.value === emptyText) {
430             me.setRawValue('');
431             isEmpty = true;
432             inputEl.removeCls(me.emptyCls);
433         } else if (Ext.supports.Placeholder) {
434             me.inputEl.removeCls(me.emptyCls);
435         }
436         if (me.selectOnFocus || isEmpty) {
437             inputEl.dom.select();
438         }
439     },
440
441     onFocus: function() {
442         var me = this;
443         me.callParent(arguments);
444         if (me.emptyText) {
445             me.autoSize();
446         }
447     },
448
449     // private
450     postBlur : function(){
451         this.applyEmptyText();
452     },
453
454     // private
455     filterKeys : function(e){
456         /*
457          * On European keyboards, the right alt key, Alt Gr, is used to type certain special characters.
458          * JS detects a keypress of this as ctrlKey & altKey. As such, we check that alt isn't pressed
459          * so we can still process these special characters.
460          */
461         if (e.ctrlKey && !e.altKey) {
462             return;
463         }
464         var key = e.getKey(),
465             charCode = String.fromCharCode(e.getCharCode());
466
467         if(Ext.isGecko && (e.isNavKeyPress() || key === e.BACKSPACE || (key === e.DELETE && e.button === -1))){
468             return;
469         }
470
471         if(!Ext.isGecko && e.isSpecialKey() && !charCode){
472             return;
473         }
474         if(!this.maskRe.test(charCode)){
475             e.stopEvent();
476         }
477     },
478
479     /**
480      * Returns the raw String value of the field, without performing any normalization, conversion, or validation. Gets
481      * the current value of the input element if the field has been rendered, ignoring the value if it is the
482      * {@link #emptyText}. To get a normalized and converted value see {@link #getValue}.
483      * @return {String} The raw String value of the field
484      */
485     getRawValue: function() {
486         var me = this,
487             v = me.callParent();
488         if (v === me.emptyText) {
489             v = '';
490         }
491         return v;
492     },
493
494     /**
495      * Sets a data value into the field and runs the change detection and validation. Also applies any configured
496      * {@link #emptyText} for text fields. To set the value directly without these inspections see {@link #setRawValue}.
497      * @param {Object} value The value to set
498      * @return {Ext.form.field.Text} this
499      */
500     setValue: function(value) {
501         var me = this,
502             inputEl = me.inputEl;
503
504         if (inputEl && me.emptyText && !Ext.isEmpty(value)) {
505             inputEl.removeCls(me.emptyCls);
506         }
507
508         me.callParent(arguments);
509
510         me.applyEmptyText();
511         return me;
512     },
513
514     /**
515      * Validates a value according to the field's validation rules and returns an array of errors
516      * for any failing validations. Validation rules are processed in the following order:
517      *
518      * 1. **Field specific validator**
519      *
520      *     A validator offers a way to customize and reuse a validation specification.
521      *     If a field is configured with a `{@link #validator}`
522      *     function, it will be passed the current field value.  The `{@link #validator}`
523      *     function is expected to return either:
524      *
525      *     - Boolean `true`  if the value is valid (validation continues).
526      *     - a String to represent the invalid message if invalid (validation halts).
527      *
528      * 2. **Basic Validation**
529      *
530      *     If the `{@link #validator}` has not halted validation,
531      *     basic validation proceeds as follows:
532      *
533      *     - `{@link #allowBlank}` : (Invalid message = `{@link #emptyText}`)
534      *
535      *         Depending on the configuration of `{@link #allowBlank}`, a
536      *         blank field will cause validation to halt at this step and return
537      *         Boolean true or false accordingly.
538      *
539      *     - `{@link #minLength}` : (Invalid message = `{@link #minLengthText}`)
540      *
541      *         If the passed value does not satisfy the `{@link #minLength}`
542      *         specified, validation halts.
543      *
544      *     -  `{@link #maxLength}` : (Invalid message = `{@link #maxLengthText}`)
545      *
546      *         If the passed value does not satisfy the `{@link #maxLength}`
547      *         specified, validation halts.
548      *
549      * 3. **Preconfigured Validation Types (VTypes)**
550      *
551      *     If none of the prior validation steps halts validation, a field
552      *     configured with a `{@link #vtype}` will utilize the
553      *     corresponding {@link Ext.form.field.VTypes VTypes} validation function.
554      *     If invalid, either the field's `{@link #vtypeText}` or
555      *     the VTypes vtype Text property will be used for the invalid message.
556      *     Keystrokes on the field will be filtered according to the VTypes
557      *     vtype Mask property.
558      *
559      * 4. **Field specific regex test**
560      *
561      *     If none of the prior validation steps halts validation, a field's
562      *     configured <code>{@link #regex}</code> test will be processed.
563      *     The invalid message for this test is configured with `{@link #regexText}`
564      *
565      * @param {Object} value The value to validate. The processed raw value will be used if nothing is passed.
566      * @return {String[]} Array of any validation errors
567      */
568     getErrors: function(value) {
569         var me = this,
570             errors = me.callParent(arguments),
571             validator = me.validator,
572             emptyText = me.emptyText,
573             allowBlank = me.allowBlank,
574             vtype = me.vtype,
575             vtypes = Ext.form.field.VTypes,
576             regex = me.regex,
577             format = Ext.String.format,
578             msg;
579
580         value = value || me.processRawValue(me.getRawValue());
581
582         if (Ext.isFunction(validator)) {
583             msg = validator.call(me, value);
584             if (msg !== true) {
585                 errors.push(msg);
586             }
587         }
588
589         if (value.length < 1 || value === emptyText) {
590             if (!allowBlank) {
591                 errors.push(me.blankText);
592             }
593             //if value is blank, there cannot be any additional errors
594             return errors;
595         }
596
597         if (value.length < me.minLength) {
598             errors.push(format(me.minLengthText, me.minLength));
599         }
600
601         if (value.length > me.maxLength) {
602             errors.push(format(me.maxLengthText, me.maxLength));
603         }
604
605         if (vtype) {
606             if(!vtypes[vtype](value, me)){
607                 errors.push(me.vtypeText || vtypes[vtype +'Text']);
608             }
609         }
610
611         if (regex && !regex.test(value)) {
612             errors.push(me.regexText || me.invalidText);
613         }
614
615         return errors;
616     },
617
618     /**
619      * Selects text in this field
620      * @param {Number} [start=0] The index where the selection should start
621      * @param {Number} [end] The index where the selection should end (defaults to the text length)
622      */
623     selectText : function(start, end){
624         var me = this,
625             v = me.getRawValue(),
626             doFocus = true,
627             el = me.inputEl.dom,
628             undef,
629             range;
630
631         if (v.length > 0) {
632             start = start === undef ? 0 : start;
633             end = end === undef ? v.length : end;
634             if (el.setSelectionRange) {
635                 el.setSelectionRange(start, end);
636             }
637             else if(el.createTextRange) {
638                 range = el.createTextRange();
639                 range.moveStart('character', start);
640                 range.moveEnd('character', end - v.length);
641                 range.select();
642             }
643             doFocus = Ext.isGecko || Ext.isOpera;
644         }
645         if (doFocus) {
646             me.focus();
647         }
648     },
649
650     /**
651      * Automatically grows the field to accomodate the width of the text up to the maximum field width allowed. This
652      * only takes effect if {@link #grow} = true, and fires the {@link #autosize} event if the width changes.
653      */
654     autoSize: function() {
655         var me = this,
656             width;
657         if (me.grow && me.rendered) {
658             me.doComponentLayout();
659             width = me.inputEl.getWidth();
660             if (width !== me.lastInputWidth) {
661                 me.fireEvent('autosize', width);
662                 me.lastInputWidth = width;
663             }
664         }
665     },
666
667     initAria: function() {
668         this.callParent();
669         this.getActionEl().dom.setAttribute('aria-required', this.allowBlank === false);
670     },
671
672     /**
673      * To get the natural width of the inputEl, we do a simple calculation based on the 'size' config. We use
674      * hard-coded numbers to approximate what browsers do natively, to avoid having to read any styles which would hurt
675      * performance. Overrides Labelable method.
676      * @protected
677      */
678     getBodyNaturalWidth: function() {
679         return Math.round(this.size * 6.5) + 20;
680     }
681
682 });
683