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