Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / Editor.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.Editor
17  * @extends Ext.Component
18  *
19  * <p>
20  * The Editor class is used to provide inline editing for elements on the page. The editor
21  * is backed by a {@link Ext.form.field.Field} that will be displayed to edit the underlying content.
22  * The editor is a floating Component, when the editor is shown it is automatically aligned to
23  * display over the top of the bound element it is editing. The Editor contains several options
24  * for how to handle key presses:
25  * <ul>
26  * <li>{@link #completeOnEnter}</li>
27  * <li>{@link #cancelOnEsc}</li>
28  * <li>{@link #swallowKeys}</li>
29  * </ul>
30  * It also has options for how to use the value once the editor has been activated:
31  * <ul>
32  * <li>{@link #revertInvalid}</li>
33  * <li>{@link #ignoreNoChange}</li>
34  * <li>{@link #updateEl}</li>
35  * </ul>
36  * Sample usage:
37  * </p>
38  * <pre><code>
39 var editor = new Ext.Editor({
40     updateEl: true, // update the innerHTML of the bound element when editing completes
41     field: {
42         xtype: 'textfield'
43     }
44 });
45 var el = Ext.get('my-text'); // The element to 'edit'
46 editor.startEdit(el); // The value of the field will be taken as the innerHTML of the element.
47  * </code></pre>
48  * {@img Ext.Editor/Ext.Editor.png Ext.Editor component}
49  *
50  */
51 Ext.define('Ext.Editor', {
52
53     /* Begin Definitions */
54
55     extend: 'Ext.Component',
56
57     alias: 'widget.editor',
58
59     requires: ['Ext.layout.component.Editor'],
60
61     /* End Definitions */
62
63    componentLayout: 'editor',
64
65     /**
66     * @cfg {Ext.form.field.Field} field
67     * The Field object (or descendant) or config object for field
68     */
69
70     /**
71      * @cfg {Boolean} allowBlur
72      * True to {@link #completeEdit complete the editing process} if in edit mode when the
73      * field is blurred. Defaults to <tt>true</tt>.
74      */
75     allowBlur: true,
76
77     /**
78      * @cfg {Boolean/Object} autoSize
79      * True for the editor to automatically adopt the size of the underlying field. Otherwise, an object
80      * can be passed to indicate where to get each dimension. The available properties are 'boundEl' and
81      * 'field'. If a dimension is not specified, it will use the underlying height/width specified on
82      * the editor object.
83      * Examples:
84      * <pre><code>
85 autoSize: true // The editor will be sized to the height/width of the field
86
87 height: 21,
88 autoSize: {
89     width: 'boundEl' // The width will be determined by the width of the boundEl, the height from the editor (21)
90 }
91
92 autoSize: {
93     width: 'field', // Width from the field
94     height: 'boundEl' // Height from the boundEl
95 }
96      * </pre></code>
97      */
98
99     /**
100      * @cfg {Boolean} revertInvalid
101      * True to automatically revert the field value and cancel the edit when the user completes an edit and the field
102      * validation fails (defaults to true)
103      */
104     revertInvalid: true,
105
106     /**
107      * @cfg {Boolean} ignoreNoChange
108      * True to skip the edit completion process (no save, no events fired) if the user completes an edit and
109      * the value has not changed (defaults to false).  Applies only to string values - edits for other data types
110      * will never be ignored.
111      */
112
113     /**
114      * @cfg {Boolean} hideEl
115      * False to keep the bound element visible while the editor is displayed (defaults to true)
116      */
117
118     /**
119      * @cfg {Mixed} value
120      * The data value of the underlying field (defaults to "")
121      */
122     value : '',
123
124     /**
125      * @cfg {String} alignment
126      * The position to align to (see {@link Ext.core.Element#alignTo} for more details, defaults to "c-c?").
127      */
128     alignment: 'c-c?',
129
130     /**
131      * @cfg {Array} offsets
132      * The offsets to use when aligning (see {@link Ext.core.Element#alignTo} for more details. Defaults to <tt>[0, 0]</tt>.
133      */
134     offsets: [0, 0],
135
136     /**
137      * @cfg {Boolean/String} shadow "sides" for sides/bottom only, "frame" for 4-way shadow, and "drop"
138      * for bottom-right shadow (defaults to "frame")
139      */
140     shadow : 'frame',
141
142     /**
143      * @cfg {Boolean} constrain True to constrain the editor to the viewport
144      */
145     constrain : false,
146
147     /**
148      * @cfg {Boolean} swallowKeys Handle the keydown/keypress events so they don't propagate (defaults to true)
149      */
150     swallowKeys : true,
151
152     /**
153      * @cfg {Boolean} completeOnEnter True to complete the edit when the enter key is pressed. Defaults to <tt>true</tt>.
154      */
155     completeOnEnter : true,
156
157     /**
158      * @cfg {Boolean} cancelOnEsc True to cancel the edit when the escape key is pressed. Defaults to <tt>true</tt>.
159      */
160     cancelOnEsc : true,
161
162     /**
163      * @cfg {Boolean} updateEl True to update the innerHTML of the bound element when the update completes (defaults to false)
164      */
165     updateEl : false,
166
167     /**
168      * @cfg {Mixed} parentEl An element to render to. Defaults to the <tt>document.body</tt>.
169      */
170
171     // private overrides
172     hidden: true,
173     baseCls: Ext.baseCSSPrefix + 'editor',
174
175     initComponent : function() {
176         var me = this,
177             field = me.field = Ext.ComponentManager.create(me.field, 'textfield');
178
179         Ext.apply(field, {
180             inEditor: true,
181             msgTarget: field.msgTarget == 'title' ? 'title' :  'qtip'
182         });
183         me.mon(field, {
184             scope: me,
185             blur: {
186                 fn: me.onBlur,
187                 // slight delay to avoid race condition with startEdits (e.g. grid view refresh)
188                 delay: 1
189             },
190             specialkey: me.onSpecialKey
191         });
192
193         if (field.grow) {
194             me.mon(field, 'autosize', me.onAutoSize,  me, {delay: 1});
195         }
196         me.floating = {
197             constrain: me.constrain
198         };
199
200         me.callParent(arguments);
201
202         me.addEvents(
203             /**
204              * @event beforestartedit
205              * Fires when editing is initiated, but before the value changes.  Editing can be canceled by returning
206              * false from the handler of this event.
207              * @param {Ext.Editor} this
208              * @param {Ext.core.Element} boundEl The underlying element bound to this editor
209              * @param {Mixed} value The field value being set
210              */
211             'beforestartedit',
212             /**
213              * @event startedit
214              * Fires when this editor is displayed
215              * @param {Ext.Editor} this
216              * @param {Ext.core.Element} boundEl The underlying element bound to this editor
217              * @param {Mixed} value The starting field value
218              */
219             'startedit',
220             /**
221              * @event beforecomplete
222              * Fires after a change has been made to the field, but before the change is reflected in the underlying
223              * field.  Saving the change to the field can be canceled by returning false from the handler of this event.
224              * Note that if the value has not changed and ignoreNoChange = true, the editing will still end but this
225              * event will not fire since no edit actually occurred.
226              * @param {Editor} this
227              * @param {Mixed} value The current field value
228              * @param {Mixed} startValue The original field value
229              */
230             'beforecomplete',
231             /**
232              * @event complete
233              * Fires after editing is complete and any changed value has been written to the underlying field.
234              * @param {Ext.Editor} this
235              * @param {Mixed} value The current field value
236              * @param {Mixed} startValue The original field value
237              */
238             'complete',
239             /**
240              * @event canceledit
241              * Fires after editing has been canceled and the editor's value has been reset.
242              * @param {Ext.Editor} this
243              * @param {Mixed} value The user-entered field value that was discarded
244              * @param {Mixed} startValue The original field value that was set back into the editor after cancel
245              */
246             'canceledit',
247             /**
248              * @event specialkey
249              * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed.  You can check
250              * {@link Ext.EventObject#getKey} to determine which key was pressed.
251              * @param {Ext.Editor} this
252              * @param {Ext.form.field.Field} The field attached to this editor
253              * @param {Ext.EventObject} event The event object
254              */
255             'specialkey'
256         );
257     },
258
259     // private
260     onAutoSize: function(){
261         this.doComponentLayout();
262     },
263
264     // private
265     onRender : function(ct, position) {
266         var me = this,
267             field = me.field;
268
269         me.callParent(arguments);
270
271         field.render(me.el);
272         //field.hide();
273         // Ensure the field doesn't get submitted as part of any form
274         field.inputEl.dom.name = '';
275         if (me.swallowKeys) {
276             field.inputEl.swallowEvent([
277                 'keypress', // *** Opera
278                 'keydown'   // *** all other browsers
279             ]);
280         }
281     },
282
283     // private
284     onSpecialKey : function(field, event) {
285         var me = this,
286             key = event.getKey(),
287             complete = me.completeOnEnter && key == event.ENTER,
288             cancel = me.cancelOnEsc && key == event.ESC;
289
290         if (complete || cancel) {
291             event.stopEvent();
292             // Must defer this slightly to prevent exiting edit mode before the field's own
293             // key nav can handle the enter key, e.g. selecting an item in a combobox list
294             Ext.defer(function() {
295                 if (complete) {
296                     me.completeEdit();
297                 } else {
298                     me.cancelEdit();
299                 }
300                 if (field.triggerBlur) {
301                     field.triggerBlur();
302                 }
303             }, 10);
304         }
305
306         this.fireEvent('specialkey', this, field, event);
307     },
308
309     /**
310      * Starts the editing process and shows the editor.
311      * @param {Mixed} el The element to edit
312      * @param {String} value (optional) A value to initialize the editor with. If a value is not provided, it defaults
313       * to the innerHTML of el.
314      */
315     startEdit : function(el, value) {
316         var me = this,
317             field = me.field;
318
319         me.completeEdit();
320         me.boundEl = Ext.get(el);
321         value = Ext.isDefined(value) ? value : me.boundEl.dom.innerHTML;
322
323         if (!me.rendered) {
324             me.render(me.parentEl || document.body);
325         }
326
327         if (me.fireEvent('beforestartedit', me, me.boundEl, value) !== false) {
328             me.startValue = value;
329             me.show();
330             field.reset();
331             field.setValue(value);
332             me.realign(true);
333             field.focus(false, 10);
334             if (field.autoSize) {
335                 field.autoSize();
336             }
337             me.editing = true;
338         }
339     },
340
341     /**
342      * Realigns the editor to the bound field based on the current alignment config value.
343      * @param {Boolean} autoSize (optional) True to size the field to the dimensions of the bound element.
344      */
345     realign : function(autoSize) {
346         var me = this;
347         if (autoSize === true) {
348             me.doComponentLayout();
349         }
350         me.alignTo(me.boundEl, me.alignment, me.offsets);
351     },
352
353     /**
354      * Ends the editing process, persists the changed value to the underlying field, and hides the editor.
355      * @param {Boolean} remainVisible Override the default behavior and keep the editor visible after edit (defaults to false)
356      */
357     completeEdit : function(remainVisible) {
358         var me = this,
359             field = me.field,
360             value;
361
362         if (!me.editing) {
363             return;
364         }
365
366         // Assert combo values first
367         if (field.assertValue) {
368             field.assertValue();
369         }
370
371         value = me.getValue();
372         if (!field.isValid()) {
373             if (me.revertInvalid !== false) {
374                 me.cancelEdit(remainVisible);
375             }
376             return;
377         }
378
379         if (String(value) === String(me.startValue) && me.ignoreNoChange) {
380             me.hideEdit(remainVisible);
381             return;
382         }
383
384         if (me.fireEvent('beforecomplete', me, value, me.startValue) !== false) {
385             // Grab the value again, may have changed in beforecomplete
386             value = me.getValue();
387             if (me.updateEl && me.boundEl) {
388                 me.boundEl.update(value);
389             }
390             me.hideEdit(remainVisible);
391             me.fireEvent('complete', me, value, me.startValue);
392         }
393     },
394
395     // private
396     onShow : function() {
397         var me = this;
398
399         me.callParent(arguments);
400         if (me.hideEl !== false) {
401             me.boundEl.hide();
402         }
403         me.fireEvent("startedit", me.boundEl, me.startValue);
404     },
405
406     /**
407      * Cancels the editing process and hides the editor without persisting any changes.  The field value will be
408      * reverted to the original starting value.
409      * @param {Boolean} remainVisible Override the default behavior and keep the editor visible after
410      * cancel (defaults to false)
411      */
412     cancelEdit : function(remainVisible) {
413         var me = this,
414             startValue = me.startValue,
415             value;
416
417         if (me.editing) {
418             value = me.getValue();
419             me.setValue(startValue);
420             me.hideEdit(remainVisible);
421             me.fireEvent('canceledit', me, value, startValue);
422         }
423     },
424
425     // private
426     hideEdit: function(remainVisible) {
427         if (remainVisible !== true) {
428             this.editing = false;
429             this.hide();
430         }
431     },
432
433     // private
434     onBlur : function() {
435         var me = this;
436
437         // selectSameEditor flag allows the same editor to be started without onBlur firing on itself
438         if(me.allowBlur === true && me.editing && me.selectSameEditor !== true) {
439             me.completeEdit();
440         }
441     },
442
443     // private
444     onHide : function() {
445         var me = this,
446             field = me.field;
447
448         if (me.editing) {
449             me.completeEdit();
450             return;
451         }
452         field.blur();
453         if (field.collapse) {
454             field.collapse();
455         }
456
457         //field.hide();
458         if (me.hideEl !== false) {
459             me.boundEl.show();
460         }
461         me.callParent(arguments);
462     },
463
464     /**
465      * Sets the data value of the editor
466      * @param {Mixed} value Any valid value supported by the underlying field
467      */
468     setValue : function(value) {
469         this.field.setValue(value);
470     },
471
472     /**
473      * Gets the data value of the editor
474      * @return {Mixed} The data value
475      */
476     getValue : function() {
477         return this.field.getValue();
478     },
479
480     beforeDestroy : function() {
481         var me = this;
482
483         Ext.destroy(me.field);
484         delete me.field;
485         delete me.parentEl;
486         delete me.boundEl;
487
488         me.callParent(arguments);
489     }
490 });