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