Upgrade to ExtJS 3.0.0 - Released 07/06/2009
[extjs.git] / src / widgets / form / Field.js
1 /*!
2  * Ext JS Library 3.0.0
3  * Copyright(c) 2006-2009 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 /**
8  * @class Ext.form.Field
9  * @extends Ext.BoxComponent
10  * Base class for form fields that provides default event handling, sizing, value handling and other functionality.
11  * @constructor
12  * Creates a new Field
13  * @param {Object} config Configuration options
14  * @xtype field
15  */
16 Ext.form.Field = Ext.extend(Ext.BoxComponent,  {
17     /**
18      * @cfg {String} inputType The type attribute for input fields -- e.g. radio, text, password, file (defaults
19      * to "text"). The types "file" and "password" must be used to render those field types currently -- there are
20      * no separate Ext components for those. Note that if you use <tt>inputType:'file'</tt>, {@link #emptyText}
21      * is not supported and should be avoided.
22      */
23     /**
24      * @cfg {Number} tabIndex The tabIndex for this field. Note this only applies to fields that are rendered,
25      * not those which are built via applyTo (defaults to undefined).
26      */
27     /**
28      * @cfg {Mixed} value A value to initialize this field with (defaults to undefined).
29      */
30     /**
31      * @cfg {String} name The field's HTML name attribute (defaults to "").
32      * <b>Note</b>: this property must be set if this field is to be automatically included with
33      * {@link Ext.form.BasicForm#submit form submit()}.
34      */
35     /**
36      * @cfg {String} cls A custom CSS class to apply to the field's underlying element (defaults to "").
37      */
38
39     /**
40      * @cfg {String} invalidClass The CSS class to use when marking a field invalid (defaults to "x-form-invalid")
41      */
42     invalidClass : "x-form-invalid",
43     /**
44      * @cfg {String} invalidText The error text to use when marking a field invalid and no message is provided
45      * (defaults to "The value in this field is invalid")
46      */
47     invalidText : "The value in this field is invalid",
48     /**
49      * @cfg {String} focusClass The CSS class to use when the field receives focus (defaults to "x-form-focus")
50      */
51     focusClass : "x-form-focus",
52     /**
53      * @cfg {String/Boolean} validationEvent The event that should initiate field validation. Set to false to disable
54       automatic validation (defaults to "keyup").
55      */
56     validationEvent : "keyup",
57     /**
58      * @cfg {Boolean} validateOnBlur Whether the field should validate when it loses focus (defaults to true).
59      */
60     validateOnBlur : true,
61     /**
62      * @cfg {Number} validationDelay The length of time in milliseconds after user input begins until validation
63      * is initiated (defaults to 250)
64      */
65     validationDelay : 250,
66     /**
67      * @cfg {String/Object} autoCreate <p>A {@link Ext.DomHelper DomHelper} element spec, or true for a default
68      * element spec. Used to create the {@link Ext.Component#getEl Element} which will encapsulate this Component.
69      * See <tt>{@link Ext.Component#autoEl autoEl}</tt> for details.  Defaults to:</p>
70      * <pre><code>{tag: "input", type: "text", size: "20", autocomplete: "off"}</code></pre>
71      */
72     defaultAutoCreate : {tag: "input", type: "text", size: "20", autocomplete: "off"},
73     /**
74      * @cfg {String} fieldClass The default CSS class for the field (defaults to "x-form-field")
75      */
76     fieldClass : "x-form-field",
77     /**
78      * @cfg {String} msgTarget The location where error text should display.  Should be one of the following values
79      * (defaults to 'qtip'):
80      *<pre>
81 Value         Description
82 -----------   ----------------------------------------------------------------------
83 qtip          Display a quick tip when the user hovers over the field
84 title         Display a default browser title attribute popup
85 under         Add a block div beneath the field containing the error text
86 side          Add an error icon to the right of the field with a popup on hover
87 [element id]  Add the error text directly to the innerHTML of the specified element
88 </pre>
89      */
90     msgTarget : 'qtip',
91     /**
92      * @cfg {String} msgFx <b>Experimental</b> The effect used when displaying a validation message under the field
93      * (defaults to 'normal').
94      */
95     msgFx : 'normal',
96     /**
97      * @cfg {Boolean} readOnly <tt>true</tt> to mark the field as readOnly in HTML
98      * (defaults to <tt>false</tt>).
99      * <br><p><b>Note</b>: this only sets the element's readOnly DOM attribute.
100      * Setting <code>readOnly=true</code>, for example, will not disable triggering a
101      * ComboBox or DateField; it gives you the option of forcing the user to choose
102      * via the trigger without typing in the text box. To hide the trigger use
103      * <code>{@link Ext.form.TriggerField#hideTrigger hideTrigger}</code>.</p>
104      */
105     readOnly : false,
106     /**
107      * @cfg {Boolean} disabled True to disable the field (defaults to false).
108      * <p>Be aware that conformant with the <a href="http://www.w3.org/TR/html401/interact/forms.html#h-17.12.1">HTML specification</a>,
109      * disabled Fields will not be {@link Ext.form.BasicForm#submit submitted}.</p>
110      */
111     disabled : false,
112
113     // private
114     isFormField : true,
115
116     // private
117     hasFocus : false,
118
119     // private
120     initComponent : function(){
121         Ext.form.Field.superclass.initComponent.call(this);
122         this.addEvents(
123             /**
124              * @event focus
125              * Fires when this field receives input focus.
126              * @param {Ext.form.Field} this
127              */
128             'focus',
129             /**
130              * @event blur
131              * Fires when this field loses input focus.
132              * @param {Ext.form.Field} this
133              */
134             'blur',
135             /**
136              * @event specialkey
137              * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed.
138              * To handle other keys see {@link Ext.Panel#keys} or {@link Ext.KeyMap}.
139              * You can check {@link Ext.EventObject#getKey} to determine which key was pressed.
140              * For example: <pre><code>
141 var form = new Ext.form.FormPanel({
142     ...
143     items: [{
144             fieldLabel: 'Field 1',
145             name: 'field1',
146             allowBlank: false
147         },{
148             fieldLabel: 'Field 2',
149             name: 'field2',
150             listeners: {
151                 specialkey: function(field, e){
152                     // e.HOME, e.END, e.PAGE_UP, e.PAGE_DOWN,
153                     // e.TAB, e.ESC, arrow keys: e.LEFT, e.RIGHT, e.UP, e.DOWN
154                     if (e.{@link Ext.EventObject#getKey getKey()} == e.ENTER) {
155                         var form = field.ownerCt.getForm();
156                         form.submit();
157                     }
158                 }
159             }
160         }
161     ],
162     ...
163 });
164              * </code></pre>
165              * @param {Ext.form.Field} this
166              * @param {Ext.EventObject} e The event object
167              */
168             'specialkey',
169             /**
170              * @event change
171              * Fires just before the field blurs if the field value has changed.
172              * @param {Ext.form.Field} this
173              * @param {Mixed} newValue The new value
174              * @param {Mixed} oldValue The original value
175              */
176             'change',
177             /**
178              * @event invalid
179              * Fires after the field has been marked as invalid.
180              * @param {Ext.form.Field} this
181              * @param {String} msg The validation message
182              */
183             'invalid',
184             /**
185              * @event valid
186              * Fires after the field has been validated with no errors.
187              * @param {Ext.form.Field} this
188              */
189             'valid'
190         );
191     },
192
193     /**
194      * Returns the {@link Ext.form.Field#name name} or {@link Ext.form.ComboBox#hiddenName hiddenName}
195      * attribute of the field if available.
196      * @return {String} name The field {@link Ext.form.Field#name name} or {@link Ext.form.ComboBox#hiddenName hiddenName}  
197      */
198     getName: function(){
199         return this.rendered && this.el.dom.name ? this.el.dom.name : this.name || this.id || '';
200     },
201
202     // private
203     onRender : function(ct, position){
204         if(!this.el){
205             var cfg = this.getAutoCreate();
206
207             if(!cfg.name){
208                 cfg.name = this.name || this.id;
209             }
210             if(this.inputType){
211                 cfg.type = this.inputType;
212             }
213             this.autoEl = cfg;
214         }
215         Ext.form.Field.superclass.onRender.call(this, ct, position);
216         
217         var type = this.el.dom.type;
218         if(type){
219             if(type == 'password'){
220                 type = 'text';
221             }
222             this.el.addClass('x-form-'+type);
223         }
224         if(this.readOnly){
225             this.el.dom.readOnly = true;
226         }
227         if(this.tabIndex !== undefined){
228             this.el.dom.setAttribute('tabIndex', this.tabIndex);
229         }
230
231         this.el.addClass([this.fieldClass, this.cls]);
232     },
233
234     // private
235     getItemCt : function(){
236         return this.el.up('.x-form-item', 4);
237     },
238
239     // private
240     initValue : function(){
241         if(this.value !== undefined){
242             this.setValue(this.value);
243         }else if(!Ext.isEmpty(this.el.dom.value) && this.el.dom.value != this.emptyText){
244             this.setValue(this.el.dom.value);
245         }
246         /**
247          * The original value of the field as configured in the {@link #value} configuration, or
248          * as loaded by the last form load operation if the form's {@link Ext.form.BasicForm#trackResetOnLoad trackResetOnLoad}
249          * setting is <code>true</code>.
250          * @type mixed
251          * @property originalValue
252          */
253         this.originalValue = this.getValue();
254     },
255
256     /**
257      * <p>Returns true if the value of this Field has been changed from its original value.
258      * Will return false if the field is disabled or has not been rendered yet.</p>
259      * <p>Note that if the owning {@link Ext.form.BasicForm form} was configured with
260      * {@link Ext.form.BasicForm}.{@link Ext.form.BasicForm#trackResetOnLoad trackResetOnLoad}
261      * then the <i>original value</i> is updated when the values are loaded by
262      * {@link Ext.form.BasicForm}.{@link Ext.form.BasicForm#setValues setValues}.</p>
263      * @return {Boolean} True if this field has been changed from its original value (and
264      * is not disabled), false otherwise.
265      */
266     isDirty : function() {
267         if(this.disabled || !this.rendered) {
268             return false;
269         }
270         return String(this.getValue()) !== String(this.originalValue);
271     },
272
273     // private
274     afterRender : function(){
275         Ext.form.Field.superclass.afterRender.call(this);
276         this.initEvents();
277         this.initValue();
278     },
279
280     // private
281     fireKey : function(e){
282         if(e.isSpecialKey()){
283             this.fireEvent("specialkey", this, e);
284         }
285     },
286
287     /**
288      * Resets the current field value to the originally loaded value and clears any validation messages.
289      * See {@link Ext.form.BasicForm}.{@link Ext.form.BasicForm#trackResetOnLoad trackResetOnLoad}
290      */
291     reset : function(){
292         this.setValue(this.originalValue);
293         this.clearInvalid();
294     },
295
296     // private
297     initEvents : function(){
298         this.mon(this.el, Ext.EventManager.useKeydown ? "keydown" : "keypress", this.fireKey,  this);
299         this.mon(this.el, 'focus', this.onFocus, this);
300
301         // fix weird FF/Win editor issue when changing OS window focus
302         var o = this.inEditor && Ext.isWindows && Ext.isGecko ? {buffer:10} : null;
303         this.mon(this.el, 'blur', this.onBlur, this, o);
304     },
305
306     // private
307     onFocus : function(){
308         if(this.focusClass){
309             this.el.addClass(this.focusClass);
310         }
311         if(!this.hasFocus){
312             this.hasFocus = true;
313             this.startValue = this.getValue();
314             this.fireEvent("focus", this);
315         }
316     },
317
318     // private
319     beforeBlur : Ext.emptyFn,
320
321     // private
322     onBlur : function(){
323         this.beforeBlur();
324         if(this.focusClass){
325             this.el.removeClass(this.focusClass);
326         }
327         this.hasFocus = false;
328         if(this.validationEvent !== false && this.validateOnBlur && this.validationEvent != "blur"){
329             this.validate();
330         }
331         var v = this.getValue();
332         if(String(v) !== String(this.startValue)){
333             this.fireEvent('change', this, v, this.startValue);
334         }
335         this.fireEvent("blur", this);
336     },
337
338     /**
339      * Returns whether or not the field value is currently valid
340      * @param {Boolean} preventMark True to disable marking the field invalid
341      * @return {Boolean} True if the value is valid, else false
342      */
343     isValid : function(preventMark){
344         if(this.disabled){
345             return true;
346         }
347         var restore = this.preventMark;
348         this.preventMark = preventMark === true;
349         var v = this.validateValue(this.processValue(this.getRawValue()));
350         this.preventMark = restore;
351         return v;
352     },
353
354     /**
355      * Validates the field value
356      * @return {Boolean} True if the value is valid, else false
357      */
358     validate : function(){
359         if(this.disabled || this.validateValue(this.processValue(this.getRawValue()))){
360             this.clearInvalid();
361             return true;
362         }
363         return false;
364     },
365
366     // protected - should be overridden by subclasses if necessary to prepare raw values for validation
367     processValue : function(value){
368         return value;
369     },
370
371     // private
372     // Subclasses should provide the validation implementation by overriding this
373     validateValue : function(value){
374         return true;
375     },
376
377     /**
378      * Mark this field as invalid, using {@link #msgTarget} to determine how to display the error and
379      * applying {@link #invalidClass} to the field's element.
380      * @param {String} msg (optional) The validation message (defaults to {@link #invalidText})
381      */
382     markInvalid : function(msg){
383         if(!this.rendered || this.preventMark){ // not rendered
384             return;
385         }
386         msg = msg || this.invalidText;
387
388         var mt = this.getMessageHandler();
389         if(mt){
390             mt.mark(this, msg);
391         }else if(this.msgTarget){
392             this.el.addClass(this.invalidClass);
393             var t = Ext.getDom(this.msgTarget);
394             if(t){
395                 t.innerHTML = msg;
396                 t.style.display = this.msgDisplay;
397             }
398         }
399         this.fireEvent('invalid', this, msg);
400     },
401
402     /**
403      * Clear any invalid styles/messages for this field
404      */
405     clearInvalid : function(){
406         if(!this.rendered || this.preventMark){ // not rendered
407             return;
408         }
409         this.el.removeClass(this.invalidClass);
410         var mt = this.getMessageHandler();
411         if(mt){
412             mt.clear(this);
413         }else if(this.msgTarget){
414             this.el.removeClass(this.invalidClass);
415             var t = Ext.getDom(this.msgTarget);
416             if(t){
417                 t.innerHTML = '';
418                 t.style.display = 'none';
419             }
420         }
421         this.fireEvent('valid', this);
422     },
423
424     // private
425     getMessageHandler : function(){
426         return Ext.form.MessageTargets[this.msgTarget];
427     },
428
429     // private
430     getErrorCt : function(){
431         return this.el.findParent('.x-form-element', 5, true) || // use form element wrap if available
432             this.el.findParent('.x-form-field-wrap', 5, true);   // else direct field wrap
433     },
434
435     // private
436     alignErrorIcon : function(){
437         this.errorIcon.alignTo(this.el, 'tl-tr', [2, 0]);
438     },
439
440     /**
441      * Returns the raw data value which may or may not be a valid, defined value.  To return a normalized value see {@link #getValue}.
442      * @return {Mixed} value The field value
443      */
444     getRawValue : function(){
445         var v = this.rendered ? this.el.getValue() : Ext.value(this.value, '');
446         if(v === this.emptyText){
447             v = '';
448         }
449         return v;
450     },
451
452     /**
453      * Returns the normalized data value (undefined or emptyText will be returned as '').  To return the raw value see {@link #getRawValue}.
454      * @return {Mixed} value The field value
455      */
456     getValue : function(){
457         if(!this.rendered) {
458             return this.value;
459         }
460         var v = this.el.getValue();
461         if(v === this.emptyText || v === undefined){
462             v = '';
463         }
464         return v;
465     },
466
467     /**
468      * Sets the underlying DOM field's value directly, bypassing validation.  To set the value with validation see {@link #setValue}.
469      * @param {Mixed} value The value to set
470      * @return {Mixed} value The field value that is set
471      */
472     setRawValue : function(v){
473         return (this.el.dom.value = (Ext.isEmpty(v) ? '' : v));
474     },
475
476     /**
477      * Sets a data value into the field and validates it.  To set the value directly without validation see {@link #setRawValue}.
478      * @param {Mixed} value The value to set
479      * @return {Ext.form.Field} this
480      */
481     setValue : function(v){
482         this.value = v;
483         if(this.rendered){
484             this.el.dom.value = (Ext.isEmpty(v) ? '' : v);
485             this.validate();
486         }
487         return this;
488     },
489
490     // private, does not work for all fields
491     append : function(v){
492          this.setValue([this.getValue(), v].join(''));
493     },
494
495     // private
496     adjustSize : function(w, h){
497         var s = Ext.form.Field.superclass.adjustSize.call(this, w, h);
498         s.width = this.adjustWidth(this.el.dom.tagName, s.width);
499         if(this.offsetCt){
500             var ct = this.getItemCt();
501             s.width -= ct.getFrameWidth('lr');
502             s.height -= ct.getFrameWidth('tb');
503         }
504         return s;
505     },
506
507     // private
508     adjustWidth : function(tag, w){
509         if(typeof w == 'number' && (Ext.isIE && (Ext.isIE6 || !Ext.isStrict)) && /input|textarea/i.test(tag) && !this.inEditor){
510             return w - 3;
511         }
512         return w;
513     }
514
515     /**
516      * @cfg {Boolean} autoWidth @hide
517      */
518     /**
519      * @cfg {Boolean} autoHeight @hide
520      */
521
522     /**
523      * @cfg {String} autoEl @hide
524      */
525 });
526
527
528 Ext.form.MessageTargets = {
529     'qtip' : {
530         mark: function(field, msg){
531             field.el.addClass(field.invalidClass);
532             field.el.dom.qtip = msg;
533             field.el.dom.qclass = 'x-form-invalid-tip';
534             if(Ext.QuickTips){ // fix for floating editors interacting with DND
535                 Ext.QuickTips.enable();
536             }
537         },
538         clear: function(field){
539             field.el.removeClass(field.invalidClass);
540             field.el.dom.qtip = '';
541         }
542     },
543     'title' : {
544         mark: function(field, msg){
545             field.el.addClass(field.invalidClass);
546             field.el.dom.title = msg;
547         },
548         clear: function(field){
549             field.el.dom.title = '';
550         }
551     },
552     'under' : {
553         mark: function(field, msg){
554             field.el.addClass(field.invalidClass);
555             if(!field.errorEl){
556                 var elp = field.getErrorCt();
557                 if(!elp){ // field has no container el
558                     field.el.dom.title = msg;
559                     return;
560                 }
561                 field.errorEl = elp.createChild({cls:'x-form-invalid-msg'});
562                 field.errorEl.setWidth(elp.getWidth(true)-20);
563             }
564             field.errorEl.update(msg);
565             Ext.form.Field.msgFx[field.msgFx].show(field.errorEl, field);
566         },
567         clear: function(field){
568             field.el.removeClass(field.invalidClass);
569             if(field.errorEl){
570                 Ext.form.Field.msgFx[field.msgFx].hide(field.errorEl, field);
571             }else{
572                 field.el.dom.title = '';
573             }
574         }
575     },
576     'side' : {
577         mark: function(field, msg){
578             field.el.addClass(field.invalidClass);
579             if(!field.errorIcon){
580                 var elp = field.getErrorCt();
581                 if(!elp){ // field has no container el
582                     field.el.dom.title = msg;
583                     return;
584                 }
585                 field.errorIcon = elp.createChild({cls:'x-form-invalid-icon'});
586             }
587             field.alignErrorIcon();
588             field.errorIcon.dom.qtip = msg;
589             field.errorIcon.dom.qclass = 'x-form-invalid-tip';
590             field.errorIcon.show();
591             field.on('resize', field.alignErrorIcon, field);
592         },
593         clear: function(field){
594             field.el.removeClass(field.invalidClass);
595             if(field.errorIcon){
596                 field.errorIcon.dom.qtip = '';
597                 field.errorIcon.hide();
598                 field.un('resize', field.alignErrorIcon, field);
599             }else{
600                 field.el.dom.title = '';
601             }
602         }
603     }
604 };
605
606 // anything other than normal should be considered experimental
607 Ext.form.Field.msgFx = {
608     normal : {
609         show: function(msgEl, f){
610             msgEl.setDisplayed('block');
611         },
612
613         hide : function(msgEl, f){
614             msgEl.setDisplayed(false).update('');
615         }
616     },
617
618     slide : {
619         show: function(msgEl, f){
620             msgEl.slideIn('t', {stopFx:true});
621         },
622
623         hide : function(msgEl, f){
624             msgEl.slideOut('t', {stopFx:true,useDisplay:true});
625         }
626     },
627
628     slideRight : {
629         show: function(msgEl, f){
630             msgEl.fixDisplay();
631             msgEl.alignTo(f.el, 'tl-tr');
632             msgEl.slideIn('l', {stopFx:true});
633         },
634
635         hide : function(msgEl, f){
636             msgEl.slideOut('l', {stopFx:true,useDisplay:true});
637         }
638     }
639 };
640 Ext.reg('field', Ext.form.Field);