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