Upgrade to ExtJS 3.0.0 - Released 07/06/2009
[extjs.git] / src / widgets / Editor.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.Editor
9  * @extends Ext.Component
10  * A base editor field that handles displaying/hiding on demand and has some built-in sizing and event handling logic.
11  * @constructor
12  * Create a new Editor
13  * @param {Object} config The config object
14  * @xtype editor
15  */
16 Ext.Editor = function(field, config){
17     if(field.field){
18         this.field = Ext.create(field.field, 'textfield');
19         config = Ext.apply({}, field); // copy so we don't disturb original config
20         delete config.field;
21     }else{
22         this.field = field;
23     }
24     Ext.Editor.superclass.constructor.call(this, config);
25 };
26
27 Ext.extend(Ext.Editor, Ext.Component, {
28     /**
29     * @cfg {Ext.form.Field} field
30     * The Field object (or descendant) or config object for field
31     */
32     /**
33      * @cfg {Boolean} allowBlur
34      * True to {@link #completeEdit complete the editing process} if in edit mode when the
35      * field is blurred. Defaults to <tt>false</tt>.
36      */
37     /**
38      * @cfg {Boolean/String} autoSize
39      * True for the editor to automatically adopt the size of the element being edited, "width" to adopt the width only,
40      * or "height" to adopt the height only (defaults to false)
41      */
42     /**
43      * @cfg {Boolean} revertInvalid
44      * True to automatically revert the field value and cancel the edit when the user completes an edit and the field
45      * validation fails (defaults to true)
46      */
47     /**
48      * @cfg {Boolean} ignoreNoChange
49      * True to skip the edit completion process (no save, no events fired) if the user completes an edit and
50      * the value has not changed (defaults to false).  Applies only to string values - edits for other data types
51      * will never be ignored.
52      */
53     /**
54      * @cfg {Boolean} hideEl
55      * False to keep the bound element visible while the editor is displayed (defaults to true)
56      */
57     /**
58      * @cfg {Mixed} value
59      * The data value of the underlying field (defaults to "")
60      */
61     value : "",
62     /**
63      * @cfg {String} alignment
64      * The position to align to (see {@link Ext.Element#alignTo} for more details, defaults to "c-c?").
65      */
66     alignment: "c-c?",
67     /**
68      * @cfg {Boolean/String} shadow "sides" for sides/bottom only, "frame" for 4-way shadow, and "drop"
69      * for bottom-right shadow (defaults to "frame")
70      */
71     shadow : "frame",
72     /**
73      * @cfg {Boolean} constrain True to constrain the editor to the viewport
74      */
75     constrain : false,
76     /**
77      * @cfg {Boolean} swallowKeys Handle the keydown/keypress events so they don't propagate (defaults to true)
78      */
79     swallowKeys : true,
80     /**
81      * @cfg {Boolean} completeOnEnter True to complete the edit when the enter key is pressed (defaults to false)
82      */
83     completeOnEnter : false,
84     /**
85      * @cfg {Boolean} cancelOnEsc True to cancel the edit when the escape key is pressed (defaults to false)
86      */
87     cancelOnEsc : false,
88     /**
89      * @cfg {Boolean} updateEl True to update the innerHTML of the bound element when the update completes (defaults to false)
90      */
91     updateEl : false,
92
93     initComponent : function(){
94         Ext.Editor.superclass.initComponent.call(this);
95         this.addEvents(
96             /**
97              * @event beforestartedit
98              * Fires when editing is initiated, but before the value changes.  Editing can be canceled by returning
99              * false from the handler of this event.
100              * @param {Editor} this
101              * @param {Ext.Element} boundEl The underlying element bound to this editor
102              * @param {Mixed} value The field value being set
103              */
104             "beforestartedit",
105             /**
106              * @event startedit
107              * Fires when this editor is displayed
108              * @param {Ext.Element} boundEl The underlying element bound to this editor
109              * @param {Mixed} value The starting field value
110              */
111             "startedit",
112             /**
113              * @event beforecomplete
114              * Fires after a change has been made to the field, but before the change is reflected in the underlying
115              * field.  Saving the change to the field can be canceled by returning false from the handler of this event.
116              * Note that if the value has not changed and ignoreNoChange = true, the editing will still end but this
117              * event will not fire since no edit actually occurred.
118              * @param {Editor} this
119              * @param {Mixed} value The current field value
120              * @param {Mixed} startValue The original field value
121              */
122             "beforecomplete",
123             /**
124              * @event complete
125              * Fires after editing is complete and any changed value has been written to the underlying field.
126              * @param {Editor} this
127              * @param {Mixed} value The current field value
128              * @param {Mixed} startValue The original field value
129              */
130             "complete",
131             /**
132              * @event canceledit
133              * Fires after editing has been canceled and the editor's value has been reset.
134              * @param {Editor} this
135              * @param {Mixed} value The user-entered field value that was discarded
136              * @param {Mixed} startValue The original field value that was set back into the editor after cancel
137              */
138             "canceledit",
139             /**
140              * @event specialkey
141              * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed.  You can check
142              * {@link Ext.EventObject#getKey} to determine which key was pressed.
143              * @param {Ext.form.Field} this
144              * @param {Ext.EventObject} e The event object
145              */
146             "specialkey"
147         );
148     },
149
150     // private
151     onRender : function(ct, position){
152         this.el = new Ext.Layer({
153             shadow: this.shadow,
154             cls: "x-editor",
155             parentEl : ct,
156             shim : this.shim,
157             shadowOffset: this.shadowOffset || 4,
158             id: this.id,
159             constrain: this.constrain
160         });
161         if(this.zIndex){
162             this.el.setZIndex(this.zIndex);
163         }
164         this.el.setStyle("overflow", Ext.isGecko ? "auto" : "hidden");
165         if(this.field.msgTarget != 'title'){
166             this.field.msgTarget = 'qtip';
167         }
168         this.field.inEditor = true;
169         this.field.render(this.el);
170         if(Ext.isGecko){
171             this.field.el.dom.setAttribute('autocomplete', 'off');
172         }
173         this.mon(this.field, "specialkey", this.onSpecialKey, this);
174         if(this.swallowKeys){
175             this.field.el.swallowEvent(['keydown','keypress']);
176         }
177         this.field.show();
178         this.mon(this.field, "blur", this.onBlur, this);
179         if(this.field.grow){
180                 this.mon(this.field, "autosize", this.el.sync,  this.el, {delay:1});
181         }
182     },
183
184     // private
185     onSpecialKey : function(field, e){
186         var key = e.getKey();
187         if(this.completeOnEnter && key == e.ENTER){
188             e.stopEvent();
189             this.completeEdit();
190         }else if(this.cancelOnEsc && key == e.ESC){
191             this.cancelEdit();
192         }else{
193             this.fireEvent('specialkey', field, e);
194         }
195         if(this.field.triggerBlur && (key == e.ENTER || key == e.ESC || key == e.TAB)){
196             this.field.triggerBlur();
197         }
198     },
199
200     /**
201      * Starts the editing process and shows the editor.
202      * @param {Mixed} el The element to edit
203      * @param {String} value (optional) A value to initialize the editor with. If a value is not provided, it defaults
204       * to the innerHTML of el.
205      */
206     startEdit : function(el, value){
207         if(this.editing){
208             this.completeEdit();
209         }
210         this.boundEl = Ext.get(el);
211         var v = value !== undefined ? value : this.boundEl.dom.innerHTML;
212         if(!this.rendered){
213             this.render(this.parentEl || document.body);
214         }
215         if(this.fireEvent("beforestartedit", this, this.boundEl, v) === false){
216             return;
217         }
218         this.startValue = v;
219         this.field.setValue(v);
220         this.doAutoSize();
221         this.el.alignTo(this.boundEl, this.alignment);
222         this.editing = true;
223         this.show();
224     },
225
226     // private
227     doAutoSize : function(){
228         if(this.autoSize){
229             var sz = this.boundEl.getSize();
230             switch(this.autoSize){
231                 case "width":
232                     this.setSize(sz.width,  "");
233                 break;
234                 case "height":
235                     this.setSize("",  sz.height);
236                 break;
237                 default:
238                     this.setSize(sz.width,  sz.height);
239             }
240         }
241     },
242
243     /**
244      * Sets the height and width of this editor.
245      * @param {Number} width The new width
246      * @param {Number} height The new height
247      */
248     setSize : function(w, h){
249         delete this.field.lastSize;
250         this.field.setSize(w, h);
251         if(this.el){
252             if(Ext.isGecko2 || Ext.isOpera){
253                 // prevent layer scrollbars
254                 this.el.setSize(w, h);
255             }
256             this.el.sync();
257         }
258     },
259
260     /**
261      * Realigns the editor to the bound field based on the current alignment config value.
262      */
263     realign : function(){
264         this.el.alignTo(this.boundEl, this.alignment);
265     },
266
267     /**
268      * Ends the editing process, persists the changed value to the underlying field, and hides the editor.
269      * @param {Boolean} remainVisible Override the default behavior and keep the editor visible after edit (defaults to false)
270      */
271     completeEdit : function(remainVisible){
272         if(!this.editing){
273             return;
274         }
275         var v = this.getValue();
276         if(!this.field.isValid()){
277             if(this.revertInvalid !== false){
278                 this.cancelEdit(remainVisible);
279             }
280             return;
281         }
282         if(String(v) === String(this.startValue) && this.ignoreNoChange){
283             this.hideEdit(remainVisible);
284             return;
285         }
286         if(this.fireEvent("beforecomplete", this, v, this.startValue) !== false){
287             v = this.getValue();
288             if(this.updateEl && this.boundEl){
289                 this.boundEl.update(v);
290             }
291             this.hideEdit(remainVisible);
292             this.fireEvent("complete", this, v, this.startValue);
293         }
294     },
295
296     // private
297     onShow : function(){
298         this.el.show();
299         if(this.hideEl !== false){
300             this.boundEl.hide();
301         }
302         this.field.show();
303         if(Ext.isIE && !this.fixIEFocus){ // IE has problems with focusing the first time
304             this.fixIEFocus = true;
305             this.deferredFocus.defer(50, this);
306         }else{
307             this.field.focus();
308         }
309         this.fireEvent("startedit", this.boundEl, this.startValue);
310     },
311
312     deferredFocus : function(){
313         if(this.editing){
314             this.field.focus();
315         }
316     },
317
318     /**
319      * Cancels the editing process and hides the editor without persisting any changes.  The field value will be
320      * reverted to the original starting value.
321      * @param {Boolean} remainVisible Override the default behavior and keep the editor visible after
322      * cancel (defaults to false)
323      */
324     cancelEdit : function(remainVisible){
325         if(this.editing){
326             var v = this.getValue();
327             this.setValue(this.startValue);
328             this.hideEdit(remainVisible);
329             this.fireEvent("canceledit", this, v, this.startValue);
330         }
331     },
332     
333     // private
334     hideEdit: function(remainVisible){
335         if(remainVisible !== true){
336             this.editing = false;
337             this.hide();
338         }
339     },
340
341     // private
342     onBlur : function(){
343         if(this.allowBlur !== true && this.editing){
344             this.completeEdit();
345         }
346     },
347
348     // private
349     onHide : function(){
350         if(this.editing){
351             this.completeEdit();
352             return;
353         }
354         this.field.blur();
355         if(this.field.collapse){
356             this.field.collapse();
357         }
358         this.el.hide();
359         if(this.hideEl !== false){
360             this.boundEl.show();
361         }
362     },
363
364     /**
365      * Sets the data value of the editor
366      * @param {Mixed} value Any valid value supported by the underlying field
367      */
368     setValue : function(v){
369         this.field.setValue(v);
370     },
371
372     /**
373      * Gets the data value of the editor
374      * @return {Mixed} The data value
375      */
376     getValue : function(){
377         return this.field.getValue();
378     },
379
380     beforeDestroy : function(){
381         Ext.destroy(this.field);
382         this.field = null;
383     }
384 });
385 Ext.reg('editor', Ext.Editor);