Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / grid / plugin / Editing.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  * This class provides an abstract grid editing plugin on selected {@link Ext.grid.column.Column columns}.
17  * The editable columns are specified by providing an {@link Ext.grid.column.Column#editor editor}
18  * in the {@link Ext.grid.column.Column column configuration}.
19  *
20  * **Note:** This class should not be used directly. See {@link Ext.grid.plugin.CellEditing} and
21  * {@link Ext.grid.plugin.RowEditing}.
22  */
23 Ext.define('Ext.grid.plugin.Editing', {
24     alias: 'editing.editing',
25
26     requires: [
27         'Ext.grid.column.Column',
28         'Ext.util.KeyNav'
29     ],
30
31     mixins: {
32         observable: 'Ext.util.Observable'
33     },
34
35     /**
36      * @cfg {Number} clicksToEdit
37      * The number of clicks on a grid required to display the editor.
38      */
39     clicksToEdit: 2,
40
41     // private
42     defaultFieldXType: 'textfield',
43
44     // cell, row, form
45     editStyle: '',
46
47     constructor: function(config) {
48         var me = this;
49         Ext.apply(me, config);
50
51         me.addEvents(
52             // Doc'ed in separate editing plugins
53             'beforeedit',
54
55             // Doc'ed in separate editing plugins
56             'edit',
57
58             // Doc'ed in separate editing plugins
59             'validateedit'
60         );
61         me.mixins.observable.constructor.call(me);
62         // TODO: Deprecated, remove in 5.0
63         me.relayEvents(me, ['afteredit'], 'after');
64     },
65
66     // private
67     init: function(grid) {
68         var me = this;
69
70         me.grid = grid;
71         me.view = grid.view;
72         me.initEvents();
73         me.mon(grid, 'reconfigure', me.onReconfigure, me);
74         me.onReconfigure();
75
76         grid.relayEvents(me, ['beforeedit', 'edit', 'validateedit']);
77         // Marks the grid as editable, so that the SelectionModel
78         // can make appropriate decisions during navigation
79         grid.isEditable = true;
80         grid.editingPlugin = grid.view.editingPlugin = me;
81     },
82
83     /**
84      * Fires after the grid is reconfigured
85      * @private
86      */
87     onReconfigure: function(){
88         this.initFieldAccessors(this.view.getGridColumns());
89     },
90
91     /**
92      * @private
93      * AbstractComponent calls destroy on all its plugins at destroy time.
94      */
95     destroy: function() {
96         var me = this,
97             grid = me.grid,
98             headerCt = grid.headerCt,
99             events = grid.events;
100
101         Ext.destroy(me.keyNav);
102         me.removeFieldAccessors(grid.getView().getGridColumns());
103
104         // Clear all listeners from all our events, clear all managed listeners we added to other Observables
105         me.clearListeners();
106
107         delete me.grid.editingPlugin;
108         delete me.grid.view.editingPlugin;
109         delete me.grid;
110         delete me.view;
111         delete me.editor;
112         delete me.keyNav;
113     },
114
115     // private
116     getEditStyle: function() {
117         return this.editStyle;
118     },
119
120     // private
121     initFieldAccessors: function(column) {
122         var me = this;
123
124         if (Ext.isArray(column)) {
125             Ext.Array.forEach(column, me.initFieldAccessors, me);
126             return;
127         }
128
129         // Augment the Header class to have a getEditor and setEditor method
130         // Important: Only if the header does not have its own implementation.
131         Ext.applyIf(column, {
132             getEditor: function(record, defaultField) {
133                 return me.getColumnField(this, defaultField);
134             },
135
136             setEditor: function(field) {
137                 me.setColumnField(this, field);
138             }
139         });
140     },
141
142     // private
143     removeFieldAccessors: function(column) {
144         var me = this;
145
146         if (Ext.isArray(column)) {
147             Ext.Array.forEach(column, me.removeFieldAccessors, me);
148             return;
149         }
150
151         delete column.getEditor;
152         delete column.setEditor;
153     },
154
155     // private
156     // remaps to the public API of Ext.grid.column.Column.getEditor
157     getColumnField: function(columnHeader, defaultField) {
158         var field = columnHeader.field;
159
160         if (!field && columnHeader.editor) {
161             field = columnHeader.editor;
162             delete columnHeader.editor;
163         }
164
165         if (!field && defaultField) {
166             field = defaultField;
167         }
168
169         if (field) {
170             if (Ext.isString(field)) {
171                 field = { xtype: field };
172             }
173             if (Ext.isObject(field) && !field.isFormField) {
174                 field = Ext.ComponentManager.create(field, this.defaultFieldXType);
175                 columnHeader.field = field;
176             }
177
178             Ext.apply(field, {
179                 name: columnHeader.dataIndex
180             });
181
182             return field;
183         }
184     },
185
186     // private
187     // remaps to the public API of Ext.grid.column.Column.setEditor
188     setColumnField: function(column, field) {
189         if (Ext.isObject(field) && !field.isFormField) {
190             field = Ext.ComponentManager.create(field, this.defaultFieldXType);
191         }
192         column.field = field;
193     },
194
195     // private
196     initEvents: function() {
197         var me = this;
198         me.initEditTriggers();
199         me.initCancelTriggers();
200     },
201
202     // @abstract
203     initCancelTriggers: Ext.emptyFn,
204
205     // private
206     initEditTriggers: function() {
207         var me = this,
208             view = me.view,
209             clickEvent = me.clicksToEdit === 1 ? 'click' : 'dblclick';
210
211         // Start editing
212         me.mon(view, 'cell' + clickEvent, me.startEditByClick, me);
213         view.on('render', function() {
214             me.keyNav = Ext.create('Ext.util.KeyNav', view.el, {
215                 enter: me.onEnterKey,
216                 esc: me.onEscKey,
217                 scope: me
218             });
219         }, me, { single: true });
220     },
221
222     // private
223     onEnterKey: function(e) {
224         var me = this,
225             grid = me.grid,
226             selModel = grid.getSelectionModel(),
227             record,
228             columnHeader = grid.headerCt.getHeaderAtIndex(0);
229
230         // Calculate editing start position from SelectionModel
231         // CellSelectionModel
232         if (selModel.getCurrentPosition) {
233             pos = selModel.getCurrentPosition();
234             record = grid.store.getAt(pos.row);
235             columnHeader = grid.headerCt.getHeaderAtIndex(pos.column);
236         }
237         // RowSelectionModel
238         else {
239             record = selModel.getLastSelected();
240         }
241         me.startEdit(record, columnHeader);
242     },
243
244     // private
245     onEscKey: function(e) {
246         this.cancelEdit();
247     },
248
249     // private
250     startEditByClick: function(view, cell, colIdx, record, row, rowIdx, e) {
251         this.startEdit(record, view.getHeaderAtIndex(colIdx));
252     },
253
254     /**
255      * @private
256      * @template
257      * Template method called before editing begins.
258      * @param {Object} context The current editing context
259      * @return {Boolean} Return false to cancel the editing process
260      */
261     beforeEdit: Ext.emptyFn,
262
263     /**
264      * Starts editing the specified record, using the specified Column definition to define which field is being edited.
265      * @param {Ext.data.Model/Number} record The Store data record which backs the row to be edited, or index of the record in Store.
266      * @param {Ext.grid.column.Column/Number} columnHeader The Column object defining the column to be edited, or index of the column.
267      */
268     startEdit: function(record, columnHeader) {
269         var me = this,
270             context = me.getEditingContext(record, columnHeader);
271
272         if (me.beforeEdit(context) === false || me.fireEvent('beforeedit', context) === false || context.cancel) {
273             return false;
274         }
275
276         me.context = context;
277         me.editing = true;
278     },
279
280     /**
281      * @private
282      * Collects all information necessary for any subclasses to perform their editing functions.
283      * @param record
284      * @param columnHeader
285      * @returns {Object} The editing context based upon the passed record and column
286      */
287     getEditingContext: function(record, columnHeader) {
288         var me = this,
289             grid = me.grid,
290             store = grid.store,
291             rowIdx,
292             colIdx,
293             view = grid.getView(),
294             value;
295
296         // If they'd passed numeric row, column indices, look them up.
297         if (Ext.isNumber(record)) {
298             rowIdx = record;
299             record = store.getAt(rowIdx);
300         } else {
301             rowIdx = store.indexOf(record);
302         }
303         if (Ext.isNumber(columnHeader)) {
304             colIdx = columnHeader;
305             columnHeader = grid.headerCt.getHeaderAtIndex(colIdx);
306         } else {
307             colIdx = columnHeader.getIndex();
308         }
309
310         value = record.get(columnHeader.dataIndex);
311         return {
312             grid: grid,
313             record: record,
314             field: columnHeader.dataIndex,
315             value: value,
316             row: view.getNode(rowIdx),
317             column: columnHeader,
318             rowIdx: rowIdx,
319             colIdx: colIdx
320         };
321     },
322
323     /**
324      * Cancels any active edit that is in progress.
325      */
326     cancelEdit: function() {
327         this.editing = false;
328     },
329
330     /**
331      * Completes the edit if there is an active edit in progress.
332      */
333     completeEdit: function() {
334         var me = this;
335
336         if (me.editing && me.validateEdit()) {
337             me.fireEvent('edit', me.context);
338         }
339
340         delete me.context;
341         me.editing = false;
342     },
343
344     // @abstract
345     validateEdit: function() {
346         var me = this,
347             context = me.context;
348
349         return me.fireEvent('validateedit', me, context) !== false && !context.cancel;
350     }
351 });
352