Upgrade to ExtJS 4.0.7 - Released 10/19/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.
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
103      */
104     revertInvalid: true,
105
106     /**
107      * @cfg {Boolean} [ignoreNoChange=false]
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.  Applies only to string values - edits for other data types
110      * will never be ignored.
111      */
112
113     /**
114      * @cfg {Boolean} [hideEl=true]
115      * False to keep the bound element visible while the editor is displayed
116      */
117
118     /**
119      * @cfg {Object} value
120      * The data value of the underlying field
121      */
122     value : '',
123
124     /**
125      * @cfg {String} alignment
126      * The position to align to (see {@link Ext.Element#alignTo} for more details).
127      */
128     alignment: 'c-c?',
129
130     /**
131      * @cfg {Number[]} offsets
132      * The offsets to use when aligning (see {@link Ext.Element#alignTo} for more details.
133      */
134     offsets: [0, 0],
135
136     /**
137      * @cfg {Boolean/String} shadow
138      * "sides" for sides/bottom only, "frame" for 4-way shadow, and "drop" for bottom-right shadow.
139      */
140     shadow : 'frame',
141
142     /**
143      * @cfg {Boolean} constrain
144      * True to constrain the editor to the viewport
145      */
146     constrain : false,
147
148     /**
149      * @cfg {Boolean} swallowKeys
150      * Handle the keydown/keypress events so they don't propagate
151      */
152     swallowKeys : true,
153
154     /**
155      * @cfg {Boolean} completeOnEnter
156      * True to complete the edit when the enter key is pressed.
157      */
158     completeOnEnter : true,
159
160     /**
161      * @cfg {Boolean} cancelOnEsc
162      * True to cancel the edit when the escape key is pressed.
163      */
164     cancelOnEsc : true,
165
166     /**
167      * @cfg {Boolean} updateEl
168      * True to update the innerHTML of the bound element when the update completes
169      */
170     updateEl : false,
171
172     /**
173      * @cfg {String/HTMLElement/Ext.Element} parentEl
174      * An element to render to. Defaults to the <tt>document.body</tt>.
175      */
176
177     // private overrides
178     hidden: true,
179     baseCls: Ext.baseCSSPrefix + 'editor',
180
181     initComponent : function() {
182         var me = this,
183             field = me.field = Ext.ComponentManager.create(me.field, 'textfield');
184
185         Ext.apply(field, {
186             inEditor: true,
187             msgTarget: field.msgTarget == 'title' ? 'title' :  'qtip'
188         });
189         me.mon(field, {
190             scope: me,
191             blur: {
192                 fn: me.onBlur,
193                 // slight delay to avoid race condition with startEdits (e.g. grid view refresh)
194                 delay: 1
195             },
196             specialkey: me.onSpecialKey
197         });
198
199         if (field.grow) {
200             me.mon(field, 'autosize', me.onAutoSize,  me, {delay: 1});
201         }
202         me.floating = {
203             constrain: me.constrain
204         };
205
206         me.callParent(arguments);
207
208         me.addEvents(
209             /**
210              * @event beforestartedit
211              * Fires when editing is initiated, but before the value changes.  Editing can be canceled by returning
212              * false from the handler of this event.
213              * @param {Ext.Editor} this
214              * @param {Ext.Element} boundEl The underlying element bound to this editor
215              * @param {Object} value The field value being set
216              */
217             'beforestartedit',
218
219             /**
220              * @event startedit
221              * Fires when this editor is displayed
222              * @param {Ext.Editor} this
223              * @param {Ext.Element} boundEl The underlying element bound to this editor
224              * @param {Object} value The starting field value
225              */
226             'startedit',
227
228             /**
229              * @event beforecomplete
230              * Fires after a change has been made to the field, but before the change is reflected in the underlying
231              * field.  Saving the change to the field can be canceled by returning false from the handler of this event.
232              * Note that if the value has not changed and ignoreNoChange = true, the editing will still end but this
233              * event will not fire since no edit actually occurred.
234              * @param {Ext.Editor} this
235              * @param {Object} value The current field value
236              * @param {Object} startValue The original field value
237              */
238             'beforecomplete',
239             /**
240              * @event complete
241              * Fires after editing is complete and any changed value has been written to the underlying field.
242              * @param {Ext.Editor} this
243              * @param {Object} value The current field value
244              * @param {Object} startValue The original field value
245              */
246             'complete',
247             /**
248              * @event canceledit
249              * Fires after editing has been canceled and the editor's value has been reset.
250              * @param {Ext.Editor} this
251              * @param {Object} value The user-entered field value that was discarded
252              * @param {Object} startValue The original field value that was set back into the editor after cancel
253              */
254             'canceledit',
255             /**
256              * @event specialkey
257              * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed.  You can check
258              * {@link Ext.EventObject#getKey} to determine which key was pressed.
259              * @param {Ext.Editor} this
260              * @param {Ext.form.field.Field} The field attached to this editor
261              * @param {Ext.EventObject} event The event object
262              */
263             'specialkey'
264         );
265     },
266
267     // private
268     onAutoSize: function(){
269         this.doComponentLayout();
270     },
271
272     // private
273     onRender : function(ct, position) {
274         var me = this,
275             field = me.field,
276             inputEl = field.inputEl;
277
278         me.callParent(arguments);
279
280         field.render(me.el);
281         //field.hide();
282         // Ensure the field doesn't get submitted as part of any form
283         if (inputEl) {
284             inputEl.dom.name = '';
285             if (me.swallowKeys) {
286                 inputEl.swallowEvent([
287                     'keypress', // *** Opera
288                     'keydown'   // *** all other browsers
289                 ]);
290             }
291         }
292     },
293
294     // private
295     onSpecialKey : function(field, event) {
296         var me = this,
297             key = event.getKey(),
298             complete = me.completeOnEnter && key == event.ENTER,
299             cancel = me.cancelOnEsc && key == event.ESC;
300
301         if (complete || cancel) {
302             event.stopEvent();
303             // Must defer this slightly to prevent exiting edit mode before the field's own
304             // key nav can handle the enter key, e.g. selecting an item in a combobox list
305             Ext.defer(function() {
306                 if (complete) {
307                     me.completeEdit();
308                 } else {
309                     me.cancelEdit();
310                 }
311                 if (field.triggerBlur) {
312                     field.triggerBlur();
313                 }
314             }, 10);
315         }
316
317         this.fireEvent('specialkey', this, field, event);
318     },
319
320     /**
321      * Starts the editing process and shows the editor.
322      * @param {String/HTMLElement/Ext.Element} el The element to edit
323      * @param {String} value (optional) A value to initialize the editor with. If a value is not provided, it defaults
324       * to the innerHTML of el.
325      */
326     startEdit : function(el, value) {
327         var me = this,
328             field = me.field;
329
330         me.completeEdit();
331         me.boundEl = Ext.get(el);
332         value = Ext.isDefined(value) ? value : me.boundEl.dom.innerHTML;
333
334         if (!me.rendered) {
335             me.render(me.parentEl || document.body);
336         }
337
338         if (me.fireEvent('beforestartedit', me, me.boundEl, value) !== false) {
339             me.startValue = value;
340             me.show();
341             field.reset();
342             field.setValue(value);
343             me.realign(true);
344             field.focus(false, 10);
345             if (field.autoSize) {
346                 field.autoSize();
347             }
348             me.editing = true;
349         }
350     },
351
352     /**
353      * Realigns the editor to the bound field based on the current alignment config value.
354      * @param {Boolean} autoSize (optional) True to size the field to the dimensions of the bound element.
355      */
356     realign : function(autoSize) {
357         var me = this;
358         if (autoSize === true) {
359             me.doComponentLayout();
360         }
361         me.alignTo(me.boundEl, me.alignment, me.offsets);
362     },
363
364     /**
365      * Ends the editing process, persists the changed value to the underlying field, and hides the editor.
366      * @param {Boolean} [remainVisible=false] Override the default behavior and keep the editor visible after edit
367      */
368     completeEdit : function(remainVisible) {
369         var me = this,
370             field = me.field,
371             value;
372
373         if (!me.editing) {
374             return;
375         }
376
377         // Assert combo values first
378         if (field.assertValue) {
379             field.assertValue();
380         }
381
382         value = me.getValue();
383         if (!field.isValid()) {
384             if (me.revertInvalid !== false) {
385                 me.cancelEdit(remainVisible);
386             }
387             return;
388         }
389
390         if (String(value) === String(me.startValue) && me.ignoreNoChange) {
391             me.hideEdit(remainVisible);
392             return;
393         }
394
395         if (me.fireEvent('beforecomplete', me, value, me.startValue) !== false) {
396             // Grab the value again, may have changed in beforecomplete
397             value = me.getValue();
398             if (me.updateEl && me.boundEl) {
399                 me.boundEl.update(value);
400             }
401             me.hideEdit(remainVisible);
402             me.fireEvent('complete', me, value, me.startValue);
403         }
404     },
405
406     // private
407     onShow : function() {
408         var me = this;
409
410         me.callParent(arguments);
411         if (me.hideEl !== false) {
412             me.boundEl.hide();
413         }
414         me.fireEvent("startedit", me.boundEl, me.startValue);
415     },
416
417     /**
418      * Cancels the editing process and hides the editor without persisting any changes.  The field value will be
419      * reverted to the original starting value.
420      * @param {Boolean} [remainVisible=false] Override the default behavior and keep the editor visible after cancel
421      */
422     cancelEdit : function(remainVisible) {
423         var me = this,
424             startValue = me.startValue,
425             value;
426
427         if (me.editing) {
428             value = me.getValue();
429             me.setValue(startValue);
430             me.hideEdit(remainVisible);
431             me.fireEvent('canceledit', me, value, startValue);
432         }
433     },
434
435     // private
436     hideEdit: function(remainVisible) {
437         if (remainVisible !== true) {
438             this.editing = false;
439             this.hide();
440         }
441     },
442
443     // private
444     onBlur : function() {
445         var me = this;
446
447         // selectSameEditor flag allows the same editor to be started without onBlur firing on itself
448         if(me.allowBlur === true && me.editing && me.selectSameEditor !== true) {
449             me.completeEdit();
450         }
451     },
452
453     // private
454     onHide : function() {
455         var me = this,
456             field = me.field;
457
458         if (me.editing) {
459             me.completeEdit();
460             return;
461         }
462         field.blur();
463         if (field.collapse) {
464             field.collapse();
465         }
466
467         //field.hide();
468         if (me.hideEl !== false) {
469             me.boundEl.show();
470         }
471         me.callParent(arguments);
472     },
473
474     /**
475      * Sets the data value of the editor
476      * @param {Object} value Any valid value supported by the underlying field
477      */
478     setValue : function(value) {
479         this.field.setValue(value);
480     },
481
482     /**
483      * Gets the data value of the editor
484      * @return {Object} The data value
485      */
486     getValue : function() {
487         return this.field.getValue();
488     },
489
490     beforeDestroy : function() {
491         var me = this;
492
493         Ext.destroy(me.field);
494         delete me.field;
495         delete me.parentEl;
496         delete me.boundEl;
497
498         me.callParent(arguments);
499     }
500 });