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