Upgrade to ExtJS 4.0.2 - Released 06/09/2011
[extjs.git] / src / grid / plugin / CellEditing.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.grid.plugin.CellEditing
17  * @extends Ext.grid.plugin.Editing
18  *
19  * The Ext.grid.plugin.CellEditing plugin injects editing at a cell level for a Grid. Only a single
20  * cell will be editable at a time. The field that will be used for the editor is defined at the
21  * {@link Ext.grid.column.Column#field field}. The editor can be a field instance or a field configuration.
22  *
23  * If an editor is not specified for a particular column then that cell will not be editable and it will
24  * be skipped when activated via the mouse or the keyboard.
25  *
26  * The editor may be shared for each column in the grid, or a different one may be specified for each column.
27  * An appropriate field type should be chosen to match the data structure that it will be editing. For example,
28  * to edit a date, it would be useful to specify {@link Ext.form.field.Date} as the editor.
29  *
30  * {@img Ext.grid.plugin.CellEditing/Ext.grid.plugin.CellEditing.png Ext.grid.plugin.CellEditing plugin}
31  *
32  * ## Example Usage
33  *
34  *     Ext.create('Ext.data.Store', {
35  *         storeId:'simpsonsStore',
36  *         fields:['name', 'email', 'phone'],
37  *         data:{'items':[
38  *             {"name":"Lisa", "email":"lisa@simpsons.com", "phone":"555-111-1224"},
39  *             {"name":"Bart", "email":"bart@simpsons.com", "phone":"555--222-1234"},
40  *             {"name":"Homer", "email":"home@simpsons.com", "phone":"555-222-1244"},
41  *             {"name":"Marge", "email":"marge@simpsons.com", "phone":"555-222-1254"}
42  *         ]},
43  *         proxy: {
44  *             type: 'memory',
45  *             reader: {
46  *                 type: 'json',
47  *                 root: 'items'
48  *             }
49  *         }
50  *     });
51  *     
52  *     Ext.create('Ext.grid.Panel', {
53  *         title: 'Simpsons',
54  *         store: Ext.data.StoreManager.lookup('simpsonsStore'),
55  *         columns: [
56  *             {header: 'Name',  dataIndex: 'name', field: 'textfield'},
57  *             {header: 'Email', dataIndex: 'email', flex:1,
58  *                 editor: {
59  *                     xtype:'textfield',
60  *                     allowBlank:false
61  *                 }
62  *             },
63  *             {header: 'Phone', dataIndex: 'phone'}
64  *         ],
65  *         selType: 'cellmodel',
66  *         plugins: [
67  *             Ext.create('Ext.grid.plugin.CellEditing', {
68  *                 clicksToEdit: 1
69  *             })
70  *         ],
71  *         height: 200,
72  *         width: 400,
73  *         renderTo: Ext.getBody()
74  *     });
75  */
76 Ext.define('Ext.grid.plugin.CellEditing', {
77     alias: 'plugin.cellediting',
78     extend: 'Ext.grid.plugin.Editing',
79     requires: ['Ext.grid.CellEditor', 'Ext.util.DelayedTask'],
80
81     constructor: function() {
82         /**
83          * @event beforeedit
84          * Fires before cell editing is triggered. The edit event object has the following properties <br />
85          * <ul style="padding:5px;padding-left:16px;">
86          * <li>grid - The grid</li>
87          * <li>record - The record being edited</li>
88          * <li>field - The field name being edited</li>
89          * <li>value - The value for the field being edited.</li>
90          * <li>row - The grid table row</li>
91          * <li>column - The grid {@link Ext.grid.column.Column Column} defining the column that is being edited.</li>
92          * <li>rowIdx - The row index that is being edited</li>
93          * <li>colIdx - The column index that is being edited</li>
94          * <li>cancel - Set this to true to cancel the edit or return false from your handler.</li>
95          * </ul>
96          * @param {Ext.grid.plugin.Editing} editor
97          * @param {Object} e An edit event (see above for description)
98          */
99         /**
100          * @event edit
101          * Fires after a cell is edited. The edit event object has the following properties <br />
102          * <ul style="padding:5px;padding-left:16px;">
103          * <li>grid - The grid</li>
104          * <li>record - The record that was edited</li>
105          * <li>field - The field name that was edited</li>
106          * <li>value - The value being set</li>
107          * <li>originalValue - The original value for the field, before the edit.</li>
108          * <li>row - The grid table row</li>
109          * <li>column - The grid {@link Ext.grid.column.Column Column} defining the column that was edited.</li>
110          * <li>rowIdx - The row index that was edited</li>
111          * <li>colIdx - The column index that was edited</li>
112          * </ul>
113          *
114          * <pre><code>
115 grid.on('edit', onEdit, this);
116
117 function onEdit(e) {
118     // execute an XHR to send/commit data to the server, in callback do (if successful):
119     e.record.commit();
120 };
121          * </code></pre>
122          * @param {Ext.grid.plugin.Editing} editor
123          * @param {Object} e An edit event (see above for description)
124          */
125         /**
126          * @event validateedit
127          * Fires after a cell is edited, but before the value is set in the record. Return false
128          * to cancel the change. The edit event object has the following properties <br />
129          * <ul style="padding:5px;padding-left:16px;">
130          * <li>grid - The grid</li>
131          * <li>record - The record being edited</li>
132          * <li>field - The field name being edited</li>
133          * <li>value - The value being set</li>
134          * <li>originalValue - The original value for the field, before the edit.</li>
135          * <li>row - The grid table row</li>
136          * <li>column - The grid {@link Ext.grid.column.Column Column} defining the column that is being edited.</li>
137          * <li>rowIdx - The row index that is being edited</li>
138          * <li>colIdx - The column index that is being edited</li>
139          * <li>cancel - Set this to true to cancel the edit or return false from your handler.</li>
140          * </ul>
141          * Usage example showing how to remove the red triangle (dirty record indicator) from some
142          * records (not all).  By observing the grid's validateedit event, it can be cancelled if
143          * the edit occurs on a targeted row (for example) and then setting the field's new value
144          * in the Record directly:
145          * <pre><code>
146 grid.on('validateedit', function(e) {
147   var myTargetRow = 6;
148
149   if (e.row == myTargetRow) {
150     e.cancel = true;
151     e.record.data[e.field] = e.value;
152   }
153 });
154          * </code></pre>
155          * @param {Ext.grid.plugin.Editing} editor
156          * @param {Object} e An edit event (see above for description)
157          */
158         this.callParent(arguments);
159         this.editors = Ext.create('Ext.util.MixedCollection', false, function(editor) {
160             return editor.editorId;
161         });
162         this.editTask = Ext.create('Ext.util.DelayedTask');
163     },
164     
165     onReconfigure: function(){
166         this.editors.clear();
167         this.callParent();    
168     },
169
170     /**
171      * @private
172      * AbstractComponent calls destroy on all its plugins at destroy time.
173      */
174     destroy: function() {
175         var me = this;
176         me.editTask.cancel();
177         me.editors.each(Ext.destroy, Ext);
178         me.editors.clear();
179         me.callParent(arguments);
180     },
181     
182     onBodyScroll: function() {
183         var ed = this.getActiveEditor();
184         if (ed && ed.field) {
185             if (ed.field.triggerBlur) {
186                 ed.field.triggerBlur();
187             } else {
188                 ed.field.blur();
189             }
190         }
191     },
192
193     // private
194     // Template method called from base class's initEvents
195     initCancelTriggers: function() {
196         var me   = this,
197             grid = me.grid,
198             view = grid.view;
199             
200         view.addElListener('mousewheel', me.cancelEdit, me);
201         me.mon(view, 'bodyscroll', me.onBodyScroll, me);
202         me.mon(grid, {
203             columnresize: me.cancelEdit,
204             columnmove: me.cancelEdit,
205             scope: me
206         });
207     },
208
209     /**
210      * Start editing the specified record, using the specified Column definition to define which field is being edited.
211      * @param {Model} record The Store data record which backs the row to be edited.
212      * @param {Model} columnHeader The Column object defining the column to be edited.
213      * @override
214      */
215     startEdit: function(record, columnHeader) {
216         var me = this,
217             ed   = me.getEditor(record, columnHeader),
218             value = record.get(columnHeader.dataIndex),
219             context = me.getEditingContext(record, columnHeader);
220
221         record = context.record;
222         columnHeader = context.column;
223
224         // Complete the edit now, before getting the editor's target
225         // cell DOM element. Completing the edit causes a view refresh.
226         me.completeEdit();
227
228         // See if the field is editable for the requested record
229         if (columnHeader && !columnHeader.getEditor(record)) {
230             return false;
231         }
232
233         if (ed) {
234             context.originalValue = context.value = value;
235             if (me.beforeEdit(context) === false || me.fireEvent('beforeedit', context) === false || context.cancel) {
236                 return false;
237             }
238
239             me.context = context;
240             me.setActiveEditor(ed);
241             me.setActiveRecord(record);
242             me.setActiveColumn(columnHeader);
243
244             // Defer, so we have some time between view scroll to sync up the editor
245             me.editTask.delay(15, ed.startEdit, ed, [me.getCell(record, columnHeader), value]);
246         } else {
247             // BrowserBug: WebKit & IE refuse to focus the element, rather
248             // it will focus it and then immediately focus the body. This
249             // temporary hack works for Webkit and IE6. IE7 and 8 are still
250             // broken
251             me.grid.getView().getEl(columnHeader).focus((Ext.isWebKit || Ext.isIE) ? 10 : false);
252         }
253     },
254
255     completeEdit: function() {
256         var activeEd = this.getActiveEditor();
257         if (activeEd) {
258             activeEd.completeEdit();
259         }
260     },
261
262     // internal getters/setters
263     setActiveEditor: function(ed) {
264         this.activeEditor = ed;
265     },
266
267     getActiveEditor: function() {
268         return this.activeEditor;
269     },
270
271     setActiveColumn: function(column) {
272         this.activeColumn = column;
273     },
274
275     getActiveColumn: function() {
276         return this.activeColumn;
277     },
278
279     setActiveRecord: function(record) {
280         this.activeRecord = record;
281     },
282
283     getActiveRecord: function() {
284         return this.activeRecord;
285     },
286
287     getEditor: function(record, column) {
288         var me = this,
289             editors = me.editors,
290             editorId = column.getItemId(),
291             editor = editors.getByKey(editorId);
292
293         if (editor) {
294             return editor;
295         } else {
296             editor = column.getEditor(record);
297             if (!editor) {
298                 return false;
299             }
300
301             // Allow them to specify a CellEditor in the Column
302             if (!(editor instanceof Ext.grid.CellEditor)) {
303                 editor = Ext.create('Ext.grid.CellEditor', {
304                     editorId: editorId,
305                     field: editor
306                 });
307             }
308             editor.parentEl = me.grid.getEditorParent();
309             // editor.parentEl should be set here.
310             editor.on({
311                 scope: me,
312                 specialkey: me.onSpecialKey,
313                 complete: me.onEditComplete,
314                 canceledit: me.cancelEdit
315             });
316             editors.add(editor);
317             return editor;
318         }
319     },
320
321     /**
322      * Get the cell (td) for a particular record and column.
323      * @param {Ext.data.Model} record
324      * @param {Ext.grid.column.Colunm} column
325      * @private
326      */
327     getCell: function(record, column) {
328         return this.grid.getView().getCell(record, column);
329     },
330
331     onSpecialKey: function(ed, field, e) {
332         var grid = this.grid,
333             sm;
334         if (e.getKey() === e.TAB) {
335             e.stopEvent();
336             sm = grid.getSelectionModel();
337             if (sm.onEditorTab) {
338                 sm.onEditorTab(this, e);
339             }
340         }
341     },
342
343     onEditComplete : function(ed, value, startValue) {
344         var me = this,
345             grid = me.grid,
346             sm = grid.getSelectionModel(),
347             activeColumn = me.getActiveColumn(),
348             dataIndex;
349
350         if (activeColumn) {
351             dataIndex = activeColumn.dataIndex;
352
353             me.setActiveEditor(null);
354             me.setActiveColumn(null);
355             me.setActiveRecord(null);
356             delete sm.wasEditing;
357     
358             if (!me.validateEdit()) {
359                 return;
360             }
361             // Only update the record if the new value is different than the
362             // startValue, when the view refreshes its el will gain focus
363             if (value !== startValue) {
364                 me.context.record.set(dataIndex, value);
365             // Restore focus back to the view's element.
366             } else {
367                 grid.getView().getEl(activeColumn).focus();
368             }
369             me.context.value = value;
370             me.fireEvent('edit', me, me.context);
371             
372
373         }
374     },
375
376     /**
377      * Cancel any active editing.
378      */
379     cancelEdit: function() {
380         var me = this,
381             activeEd = me.getActiveEditor(),
382             viewEl = me.grid.getView().getEl(me.getActiveColumn());
383
384         me.setActiveEditor(null);
385         me.setActiveColumn(null);
386         me.setActiveRecord(null);
387         if (activeEd) {
388             activeEd.cancelEdit();
389             viewEl.focus();
390         }
391     },
392
393     /**
394      * Starts editing by position (row/column)
395      * @param {Object} position A position with keys of row and column.
396      */
397     startEditByPosition: function(position) {
398         var me = this,
399             grid = me.grid,
400             sm = grid.getSelectionModel(),
401             editRecord = grid.store.getAt(position.row),
402             editColumnHeader = grid.headerCt.getHeaderAtIndex(position.column);
403
404         if (sm.selectByPosition) {
405             sm.selectByPosition(position);
406         }
407         me.startEdit(editRecord, editColumnHeader);
408     }
409 });