Upgrade to ExtJS 3.1.1 - Released 02/08/2010
[extjs.git] / examples / ux / RowEditor.js
1 /*!
2  * Ext JS Library 3.1.1
3  * Copyright(c) 2006-2010 Ext JS, LLC
4  * licensing@extjs.com
5  * http://www.extjs.com/license
6  */
7 Ext.ns('Ext.ux.grid');
8
9 /**
10  * @class Ext.ux.grid.RowEditor
11  * @extends Ext.Panel
12  * Plugin (ptype = 'roweditor') that adds the ability to rapidly edit full rows in a grid.
13  * A validation mode may be enabled which uses AnchorTips to notify the user of all
14  * validation errors at once.
15  *
16  * @ptype roweditor
17  */
18 Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, {
19     floating: true,
20     shadow: false,
21     layout: 'hbox',
22     cls: 'x-small-editor',
23     buttonAlign: 'center',
24     baseCls: 'x-row-editor',
25     elements: 'header,footer,body',
26     frameWidth: 5,
27     buttonPad: 3,
28     clicksToEdit: 'auto',
29     monitorValid: true,
30     focusDelay: 250,
31     errorSummary: true,
32
33     saveText: 'Save',
34     cancelText: 'Cancel',
35     commitChangesText: 'You need to commit or cancel your changes',
36     errorText: 'Errors',
37
38     defaults: {
39         normalWidth: true
40     },
41
42     initComponent: function(){
43         Ext.ux.grid.RowEditor.superclass.initComponent.call(this);
44         this.addEvents(
45             /**
46              * @event beforeedit
47              * Fired before the row editor is activated.
48              * If the listener returns <tt>false</tt> the editor will not be activated.
49              * @param {Ext.ux.grid.RowEditor} roweditor This object
50              * @param {Number} rowIndex The rowIndex of the row just edited
51              */
52             'beforeedit',
53             /**
54              * @event canceledit
55              * Fired when the editor is cancelled.
56              * @param {Ext.ux.grid.RowEditor} roweditor This object
57              * @param {Boolean} forced True if the cancel button is pressed, false is the editor was invalid.
58              */
59             'canceledit',
60             /**
61              * @event validateedit
62              * Fired after a row is edited and passes validation.
63              * If the listener returns <tt>false</tt> changes to the record will not be set.
64              * @param {Ext.ux.grid.RowEditor} roweditor This object
65              * @param {Object} changes Object with changes made to the record.
66              * @param {Ext.data.Record} r The Record that was edited.
67              * @param {Number} rowIndex The rowIndex of the row just edited
68              */
69             'validateedit',
70             /**
71              * @event afteredit
72              * Fired after a row is edited and passes validation.  This event is fired
73              * after the store's update event is fired with this edit.
74              * @param {Ext.ux.grid.RowEditor} roweditor This object
75              * @param {Object} changes Object with changes made to the record.
76              * @param {Ext.data.Record} r The Record that was edited.
77              * @param {Number} rowIndex The rowIndex of the row just edited
78              */
79             'afteredit'
80         );
81     },
82
83     init: function(grid){
84         this.grid = grid;
85         this.ownerCt = grid;
86         if(this.clicksToEdit === 2){
87             grid.on('rowdblclick', this.onRowDblClick, this);
88         }else{
89             grid.on('rowclick', this.onRowClick, this);
90             if(Ext.isIE){
91                 grid.on('rowdblclick', this.onRowDblClick, this);
92             }
93         }
94
95         // stopEditing without saving when a record is removed from Store.
96         grid.getStore().on('remove', function() {
97             this.stopEditing(false);
98         },this);
99
100         grid.on({
101             scope: this,
102             keydown: this.onGridKey,
103             columnresize: this.verifyLayout,
104             columnmove: this.refreshFields,
105             reconfigure: this.refreshFields,
106             beforedestroy : this.beforedestroy,
107             destroy : this.destroy,
108             bodyscroll: {
109                 buffer: 250,
110                 fn: this.positionButtons
111             }
112         });
113         grid.getColumnModel().on('hiddenchange', this.verifyLayout, this, {delay:1});
114         grid.getView().on('refresh', this.stopEditing.createDelegate(this, []));
115     },
116
117     beforedestroy: function() {
118         this.grid.getStore().un('remove', this.onStoreRemove, this);
119         this.stopEditing(false);
120         Ext.destroy(this.btns);
121     },
122
123     refreshFields: function(){
124         this.initFields();
125         this.verifyLayout();
126     },
127
128     isDirty: function(){
129         var dirty;
130         this.items.each(function(f){
131             if(String(this.values[f.id]) !== String(f.getValue())){
132                 dirty = true;
133                 return false;
134             }
135         }, this);
136         return dirty;
137     },
138
139     startEditing: function(rowIndex, doFocus){
140         if(this.editing && this.isDirty()){
141             this.showTooltip(this.commitChangesText);
142             return;
143         }
144         if(Ext.isObject(rowIndex)){
145             rowIndex = this.grid.getStore().indexOf(rowIndex);
146         }
147         if(this.fireEvent('beforeedit', this, rowIndex) !== false){
148             this.editing = true;
149             var g = this.grid, view = g.getView(),
150                 row = view.getRow(rowIndex),
151                 record = g.store.getAt(rowIndex);
152
153             this.record = record;
154             this.rowIndex = rowIndex;
155             this.values = {};
156             if(!this.rendered){
157                 this.render(view.getEditorParent());
158             }
159             var w = Ext.fly(row).getWidth();
160             this.setSize(w);
161             if(!this.initialized){
162                 this.initFields();
163             }
164             var cm = g.getColumnModel(), fields = this.items.items, f, val;
165             for(var i = 0, len = cm.getColumnCount(); i < len; i++){
166                 val = this.preEditValue(record, cm.getDataIndex(i));
167                 f = fields[i];
168                 f.setValue(val);
169                 this.values[f.id] = Ext.isEmpty(val) ? '' : val;
170             }
171             this.verifyLayout(true);
172             if(!this.isVisible()){
173                 this.setPagePosition(Ext.fly(row).getXY());
174             } else{
175                 this.el.setXY(Ext.fly(row).getXY(), {duration:0.15});
176             }
177             if(!this.isVisible()){
178                 this.show().doLayout();
179             }
180             if(doFocus !== false){
181                 this.doFocus.defer(this.focusDelay, this);
182             }
183         }
184     },
185
186     stopEditing : function(saveChanges){
187         this.editing = false;
188         if(!this.isVisible()){
189             return;
190         }
191         if(saveChanges === false || !this.isValid()){
192             this.hide();
193             this.fireEvent('canceledit', this, saveChanges === false);
194             return;
195         }
196         var changes = {},
197             r = this.record,
198             hasChange = false,
199             cm = this.grid.colModel,
200             fields = this.items.items;
201         for(var i = 0, len = cm.getColumnCount(); i < len; i++){
202             if(!cm.isHidden(i)){
203                 var dindex = cm.getDataIndex(i);
204                 if(!Ext.isEmpty(dindex)){
205                     var oldValue = r.data[dindex],
206                         value = this.postEditValue(fields[i].getValue(), oldValue, r, dindex);
207                     if(String(oldValue) !== String(value)){
208                         changes[dindex] = value;
209                         hasChange = true;
210                     }
211                 }
212             }
213         }
214         if(hasChange && this.fireEvent('validateedit', this, changes, r, this.rowIndex) !== false){
215             r.beginEdit();
216             Ext.iterate(changes, function(name, value){
217                 r.set(name, value);
218             });
219             r.endEdit();
220             this.fireEvent('afteredit', this, changes, r, this.rowIndex);
221         }
222         this.hide();
223     },
224
225     verifyLayout: function(force){
226         if(this.el && (this.isVisible() || force === true)){
227             var row = this.grid.getView().getRow(this.rowIndex);
228             this.setSize(Ext.fly(row).getWidth(), Ext.isIE ? Ext.fly(row).getHeight() + 9 : undefined);
229             var cm = this.grid.colModel, fields = this.items.items;
230             for(var i = 0, len = cm.getColumnCount(); i < len; i++){
231                 if(!cm.isHidden(i)){
232                     var adjust = 0;
233                     if(i === (len - 1)){
234                         adjust += 3; // outer padding
235                     } else{
236                         adjust += 1;
237                     }
238                     fields[i].show();
239                     fields[i].setWidth(cm.getColumnWidth(i) - adjust);
240                 } else{
241                     fields[i].hide();
242                 }
243             }
244             this.doLayout();
245             this.positionButtons();
246         }
247     },
248
249     slideHide : function(){
250         this.hide();
251     },
252
253     initFields: function(){
254         var cm = this.grid.getColumnModel(), pm = Ext.layout.ContainerLayout.prototype.parseMargins;
255         this.removeAll(false);
256         for(var i = 0, len = cm.getColumnCount(); i < len; i++){
257             var c = cm.getColumnAt(i),
258                 ed = c.getEditor();
259             if(!ed){
260                 ed = c.displayEditor || new Ext.form.DisplayField();
261             }else{
262                 ed = ed.field;
263             }
264             if(i == 0){
265                 ed.margins = pm('0 1 2 1');
266             } else if(i == len - 1){
267                 ed.margins = pm('0 0 2 1');
268             } else{
269                 ed.margins = pm('0 1 2');
270             }
271             ed.setWidth(cm.getColumnWidth(i));
272             ed.column = c;
273             if(ed.ownerCt !== this){
274                 ed.on('focus', this.ensureVisible, this);
275                 ed.on('specialkey', this.onKey, this);
276             }
277             this.insert(i, ed);
278         }
279         this.initialized = true;
280     },
281
282     onKey: function(f, e){
283         if(e.getKey() === e.ENTER){
284             this.stopEditing(true);
285             e.stopPropagation();
286         }
287     },
288
289     onGridKey: function(e){
290         if(e.getKey() === e.ENTER && !this.isVisible()){
291             var r = this.grid.getSelectionModel().getSelected();
292             if(r){
293                 var index = this.grid.store.indexOf(r);
294                 this.startEditing(index);
295                 e.stopPropagation();
296             }
297         }
298     },
299
300     ensureVisible: function(editor){
301         if(this.isVisible()){
302              this.grid.getView().ensureVisible(this.rowIndex, this.grid.colModel.getIndexById(editor.column.id), true);
303         }
304     },
305
306     onRowClick: function(g, rowIndex, e){
307         if(this.clicksToEdit == 'auto'){
308             var li = this.lastClickIndex;
309             this.lastClickIndex = rowIndex;
310             if(li != rowIndex && !this.isVisible()){
311                 return;
312             }
313         }
314         this.startEditing(rowIndex, false);
315         this.doFocus.defer(this.focusDelay, this, [e.getPoint()]);
316     },
317
318     onRowDblClick: function(g, rowIndex, e){
319         this.startEditing(rowIndex, false);
320         this.doFocus.defer(this.focusDelay, this, [e.getPoint()]);
321     },
322
323     onRender: function(){
324         Ext.ux.grid.RowEditor.superclass.onRender.apply(this, arguments);
325         this.el.swallowEvent(['keydown', 'keyup', 'keypress']);
326         this.btns = new Ext.Panel({
327             baseCls: 'x-plain',
328             cls: 'x-btns',
329             elements:'body',
330             layout: 'table',
331             width: (this.minButtonWidth * 2) + (this.frameWidth * 2) + (this.buttonPad * 4), // width must be specified for IE
332             items: [{
333                 ref: 'saveBtn',
334                 itemId: 'saveBtn',
335                 xtype: 'button',
336                 text: this.saveText,
337                 width: this.minButtonWidth,
338                 handler: this.stopEditing.createDelegate(this, [true])
339             }, {
340                 xtype: 'button',
341                 text: this.cancelText,
342                 width: this.minButtonWidth,
343                 handler: this.stopEditing.createDelegate(this, [false])
344             }]
345         });
346         this.btns.render(this.bwrap);
347     },
348
349     afterRender: function(){
350         Ext.ux.grid.RowEditor.superclass.afterRender.apply(this, arguments);
351         this.positionButtons();
352         if(this.monitorValid){
353             this.startMonitoring();
354         }
355     },
356
357     onShow: function(){
358         if(this.monitorValid){
359             this.startMonitoring();
360         }
361         Ext.ux.grid.RowEditor.superclass.onShow.apply(this, arguments);
362     },
363
364     onHide: function(){
365         Ext.ux.grid.RowEditor.superclass.onHide.apply(this, arguments);
366         this.stopMonitoring();
367         this.grid.getView().focusRow(this.rowIndex);
368     },
369
370     positionButtons: function(){
371         if(this.btns){
372             var g = this.grid,
373                 h = this.el.dom.clientHeight,
374                 view = g.getView(),
375                 scroll = view.scroller.dom.scrollLeft,
376                 bw = this.btns.getWidth(),
377                 width = Math.min(g.getWidth(), g.getColumnModel().getTotalWidth());
378
379             this.btns.el.shift({left: (width/2)-(bw/2)+scroll, top: h - 2, stopFx: true, duration:0.2});
380         }
381     },
382
383     // private
384     preEditValue : function(r, field){
385         var value = r.data[field];
386         return this.autoEncode && typeof value === 'string' ? Ext.util.Format.htmlDecode(value) : value;
387     },
388
389     // private
390     postEditValue : function(value, originalValue, r, field){
391         return this.autoEncode && typeof value == 'string' ? Ext.util.Format.htmlEncode(value) : value;
392     },
393
394     doFocus: function(pt){
395         if(this.isVisible()){
396             var index = 0,
397                 cm = this.grid.getColumnModel(),
398                 c,
399                 ed;
400             if(pt){
401                 index = this.getTargetColumnIndex(pt);
402             }
403             for(var i = index||0, len = cm.getColumnCount(); i < len; i++){
404                 c = cm.getColumnAt(i);
405                 ed = c.getEditor();
406                 if(!c.hidden && ed){
407                     ed.field.focus();
408                     break;
409                 }
410             }
411         }
412     },
413
414     getTargetColumnIndex: function(pt){
415         var grid = this.grid,
416             v = grid.view,
417             x = pt.left,
418             cms = grid.colModel.config,
419             i = 0,
420             match = false;
421         for(var len = cms.length, c; c = cms[i]; i++){
422             if(!c.hidden){
423                 if(Ext.fly(v.getHeaderCell(i)).getRegion().right >= x){
424                     match = i;
425                     break;
426                 }
427             }
428         }
429         return match;
430     },
431
432     startMonitoring : function(){
433         if(!this.bound && this.monitorValid){
434             this.bound = true;
435             Ext.TaskMgr.start({
436                 run : this.bindHandler,
437                 interval : this.monitorPoll || 200,
438                 scope: this
439             });
440         }
441     },
442
443     stopMonitoring : function(){
444         this.bound = false;
445         if(this.tooltip){
446             this.tooltip.hide();
447         }
448     },
449
450     isValid: function(){
451         var valid = true;
452         this.items.each(function(f){
453             if(!f.isValid(true)){
454                 valid = false;
455                 return false;
456             }
457         });
458         return valid;
459     },
460
461     // private
462     bindHandler : function(){
463         if(!this.bound){
464             return false; // stops binding
465         }
466         var valid = this.isValid();
467         if(!valid && this.errorSummary){
468             this.showTooltip(this.getErrorText().join(''));
469         }
470         this.btns.saveBtn.setDisabled(!valid);
471         this.fireEvent('validation', this, valid);
472     },
473
474     showTooltip: function(msg){
475         var t = this.tooltip;
476         if(!t){
477             t = this.tooltip = new Ext.ToolTip({
478                 maxWidth: 600,
479                 cls: 'errorTip',
480                 width: 300,
481                 title: this.errorText,
482                 autoHide: false,
483                 anchor: 'left',
484                 anchorToTarget: true,
485                 mouseOffset: [40,0]
486             });
487         }
488         var v = this.grid.getView(),
489             top = parseInt(this.el.dom.style.top, 10),
490             scroll = v.scroller.dom.scrollTop,
491             h = this.el.getHeight();
492
493         if(top + h >= scroll){
494             t.initTarget(this.items.last().getEl());
495             if(!t.rendered){
496                 t.show();
497                 t.hide();
498             }
499             t.body.update(msg);
500             t.doAutoWidth(20);
501             t.show();
502         }else if(t.rendered){
503             t.hide();
504         }
505     },
506
507     getErrorText: function(){
508         var data = ['<ul>'];
509         this.items.each(function(f){
510             if(!f.isValid(true)){
511                 data.push('<li>', f.getActiveError(), '</li>');
512             }
513         });
514         data.push('</ul>');
515         return data;
516     }
517 });
518 Ext.preg('roweditor', Ext.ux.grid.RowEditor);