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