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