Upgrade to ExtJS 3.3.1 - Released 11/30/2010
[extjs.git] / examples / ux / RowEditor.js
1 /*!
2  * Ext JS Library 3.3.1
3  * Copyright(c) 2006-2010 Sencha Inc.
4  * licensing@sencha.com
5  * http://www.sencha.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.stopMonitoring();
119         this.grid.getStore().un('remove', this.onStoreRemove, this);
120         this.stopEditing(false);
121         Ext.destroy(this.btns, this.tooltip);
122     },
123
124     refreshFields: function(){
125         this.initFields();
126         this.verifyLayout();
127     },
128
129     isDirty: function(){
130         var dirty;
131         this.items.each(function(f){
132             if(String(this.values[f.id]) !== String(f.getValue())){
133                 dirty = true;
134                 return false;
135             }
136         }, this);
137         return dirty;
138     },
139
140     startEditing: function(rowIndex, doFocus){
141         if(this.editing && this.isDirty()){
142             this.showTooltip(this.commitChangesText);
143             return;
144         }
145         if(Ext.isObject(rowIndex)){
146             rowIndex = this.grid.getStore().indexOf(rowIndex);
147         }
148         if(this.fireEvent('beforeedit', this, rowIndex) !== false){
149             this.editing = true;
150             var g = this.grid, view = g.getView(),
151                 row = view.getRow(rowIndex),
152                 record = g.store.getAt(rowIndex);
153
154             this.record = record;
155             this.rowIndex = rowIndex;
156             this.values = {};
157             if(!this.rendered){
158                 this.render(view.getEditorParent());
159             }
160             var w = Ext.fly(row).getWidth();
161             this.setSize(w);
162             if(!this.initialized){
163                 this.initFields();
164             }
165             var cm = g.getColumnModel(), fields = this.items.items, f, val;
166             for(var i = 0, len = cm.getColumnCount(); i < len; i++){
167                 val = this.preEditValue(record, cm.getDataIndex(i));
168                 f = fields[i];
169                 f.setValue(val);
170                 this.values[f.id] = Ext.isEmpty(val) ? '' : val;
171             }
172             this.verifyLayout(true);
173             if(!this.isVisible()){
174                 this.setPagePosition(Ext.fly(row).getXY());
175             } else{
176                 this.el.setXY(Ext.fly(row).getXY(), {duration:0.15});
177             }
178             if(!this.isVisible()){
179                 this.show().doLayout();
180             }
181             if(doFocus !== false){
182                 this.doFocus.defer(this.focusDelay, this);
183             }
184         }
185     },
186
187     stopEditing : function(saveChanges){
188         this.editing = false;
189         if(!this.isVisible()){
190             return;
191         }
192         if(saveChanges === false || !this.isValid()){
193             this.hide();
194             this.fireEvent('canceledit', this, saveChanges === false);
195             return;
196         }
197         var changes = {},
198             r = this.record,
199             hasChange = false,
200             cm = this.grid.colModel,
201             fields = this.items.items;
202         for(var i = 0, len = cm.getColumnCount(); i < len; i++){
203             if(!cm.isHidden(i)){
204                 var dindex = cm.getDataIndex(i);
205                 if(!Ext.isEmpty(dindex)){
206                     var oldValue = r.data[dindex],
207                         value = this.postEditValue(fields[i].getValue(), oldValue, r, dindex);
208                     if(String(oldValue) !== String(value)){
209                         changes[dindex] = value;
210                         hasChange = true;
211                     }
212                 }
213             }
214         }
215         if(hasChange && this.fireEvent('validateedit', this, changes, r, this.rowIndex) !== false){
216             r.beginEdit();
217             Ext.iterate(changes, function(name, value){
218                 r.set(name, value);
219             });
220             r.endEdit();
221             this.fireEvent('afteredit', this, changes, r, this.rowIndex);
222         }
223         this.hide();
224     },
225
226     verifyLayout: function(force){
227         if(this.el && (this.isVisible() || force === true)){
228             var row = this.grid.getView().getRow(this.rowIndex);
229             this.setSize(Ext.fly(row).getWidth(), Ext.isIE ? Ext.fly(row).getHeight() + 9 : undefined);
230             var cm = this.grid.colModel, fields = this.items.items;
231             for(var i = 0, len = cm.getColumnCount(); i < len; i++){
232                 if(!cm.isHidden(i)){
233                     var adjust = 0;
234                     if(i === (len - 1)){
235                         adjust += 3; // outer padding
236                     } else{
237                         adjust += 1;
238                     }
239                     fields[i].show();
240                     fields[i].setWidth(cm.getColumnWidth(i) - adjust);
241                 } else{
242                     fields[i].hide();
243                 }
244             }
245             this.doLayout();
246             this.positionButtons();
247         }
248     },
249
250     slideHide : function(){
251         this.hide();
252     },
253
254     initFields: function(){
255         var cm = this.grid.getColumnModel(), pm = Ext.layout.ContainerLayout.prototype.parseMargins;
256         this.removeAll(false);
257         for(var i = 0, len = cm.getColumnCount(); i < len; i++){
258             var c = cm.getColumnAt(i),
259                 ed = c.getEditor();
260             if(!ed){
261                 ed = c.displayEditor || new Ext.form.DisplayField();
262             }
263             if(i == 0){
264                 ed.margins = pm('0 1 2 1');
265             } else if(i == len - 1){
266                 ed.margins = pm('0 0 2 1');
267             } else{
268                 if (Ext.isIE) {
269                     ed.margins = pm('0 0 2 0');
270                 }
271                 else {
272                     ed.margins = pm('0 1 2 0');
273                 }
274             }
275             ed.setWidth(cm.getColumnWidth(i));
276             ed.column = c;
277             if(ed.ownerCt !== this){
278                 ed.on('focus', this.ensureVisible, this);
279                 ed.on('specialkey', this.onKey, this);
280             }
281             this.insert(i, ed);
282         }
283         this.initialized = true;
284     },
285
286     onKey: function(f, e){
287         if(e.getKey() === e.ENTER){
288             this.stopEditing(true);
289             e.stopPropagation();
290         }
291     },
292
293     onGridKey: function(e){
294         if(e.getKey() === e.ENTER && !this.isVisible()){
295             var r = this.grid.getSelectionModel().getSelected();
296             if(r){
297                 var index = this.grid.store.indexOf(r);
298                 this.startEditing(index);
299                 e.stopPropagation();
300             }
301         }
302     },
303
304     ensureVisible: function(editor){
305         if(this.isVisible()){
306              this.grid.getView().ensureVisible(this.rowIndex, this.grid.colModel.getIndexById(editor.column.id), true);
307         }
308     },
309
310     onRowClick: function(g, rowIndex, e){
311         if(this.clicksToEdit == 'auto'){
312             var li = this.lastClickIndex;
313             this.lastClickIndex = rowIndex;
314             if(li != rowIndex && !this.isVisible()){
315                 return;
316             }
317         }
318         this.startEditing(rowIndex, false);
319         this.doFocus.defer(this.focusDelay, this, [e.getPoint()]);
320     },
321
322     onRowDblClick: function(g, rowIndex, e){
323         this.startEditing(rowIndex, false);
324         this.doFocus.defer(this.focusDelay, this, [e.getPoint()]);
325     },
326
327     onRender: function(){
328         Ext.ux.grid.RowEditor.superclass.onRender.apply(this, arguments);
329         this.el.swallowEvent(['keydown', 'keyup', 'keypress']);
330         this.btns = new Ext.Panel({
331             baseCls: 'x-plain',
332             cls: 'x-btns',
333             elements:'body',
334             layout: 'table',
335             width: (this.minButtonWidth * 2) + (this.frameWidth * 2) + (this.buttonPad * 4), // width must be specified for IE
336             items: [{
337                 ref: 'saveBtn',
338                 itemId: 'saveBtn',
339                 xtype: 'button',
340                 text: this.saveText,
341                 width: this.minButtonWidth,
342                 handler: this.stopEditing.createDelegate(this, [true])
343             }, {
344                 xtype: 'button',
345                 text: this.cancelText,
346                 width: this.minButtonWidth,
347                 handler: this.stopEditing.createDelegate(this, [false])
348             }]
349         });
350         this.btns.render(this.bwrap);
351     },
352
353     afterRender: function(){
354         Ext.ux.grid.RowEditor.superclass.afterRender.apply(this, arguments);
355         this.positionButtons();
356         if(this.monitorValid){
357             this.startMonitoring();
358         }
359     },
360
361     onShow: function(){
362         if(this.monitorValid){
363             this.startMonitoring();
364         }
365         Ext.ux.grid.RowEditor.superclass.onShow.apply(this, arguments);
366     },
367
368     onHide: function(){
369         Ext.ux.grid.RowEditor.superclass.onHide.apply(this, arguments);
370         this.stopMonitoring();
371         this.grid.getView().focusRow(this.rowIndex);
372     },
373
374     positionButtons: function(){
375         if(this.btns){
376             var g = this.grid,
377                 h = this.el.dom.clientHeight,
378                 view = g.getView(),
379                 scroll = view.scroller.dom.scrollLeft,
380                 bw = this.btns.getWidth(),
381                 width = Math.min(g.getWidth(), g.getColumnModel().getTotalWidth());
382
383             this.btns.el.shift({left: (width/2)-(bw/2)+scroll, top: h - 2, stopFx: true, duration:0.2});
384         }
385     },
386
387     // private
388     preEditValue : function(r, field){
389         var value = r.data[field];
390         return this.autoEncode && typeof value === 'string' ? Ext.util.Format.htmlDecode(value) : value;
391     },
392
393     // private
394     postEditValue : function(value, originalValue, r, field){
395         return this.autoEncode && typeof value == 'string' ? Ext.util.Format.htmlEncode(value) : value;
396     },
397
398     doFocus: function(pt){
399         if(this.isVisible()){
400             var index = 0,
401                 cm = this.grid.getColumnModel(),
402                 c;
403             if(pt){
404                 index = this.getTargetColumnIndex(pt);
405             }
406             for(var i = index||0, len = cm.getColumnCount(); i < len; i++){
407                 c = cm.getColumnAt(i);
408                 if(!c.hidden && c.getEditor()){
409                     c.getEditor().focus();
410                     break;
411                 }
412             }
413         }
414     },
415
416     getTargetColumnIndex: function(pt){
417         var grid = this.grid,
418             v = grid.view,
419             x = pt.left,
420             cms = grid.colModel.config,
421             i = 0,
422             match = false;
423         for(var len = cms.length, c; c = cms[i]; i++){
424             if(!c.hidden){
425                 if(Ext.fly(v.getHeaderCell(i)).getRegion().right >= x){
426                     match = i;
427                     break;
428                 }
429             }
430         }
431         return match;
432     },
433
434     startMonitoring : function(){
435         if(!this.bound && this.monitorValid){
436             this.bound = true;
437             Ext.TaskMgr.start({
438                 run : this.bindHandler,
439                 interval : this.monitorPoll || 200,
440                 scope: this
441             });
442         }
443     },
444
445     stopMonitoring : function(){
446         this.bound = false;
447         if(this.tooltip){
448             this.tooltip.hide();
449         }
450     },
451
452     isValid: function(){
453         var valid = true;
454         this.items.each(function(f){
455             if(!f.isValid(true)){
456                 valid = false;
457                 return false;
458             }
459         });
460         return valid;
461     },
462
463     // private
464     bindHandler : function(){
465         if(!this.bound){
466             return false; // stops binding
467         }
468         var valid = this.isValid();
469         if(!valid && this.errorSummary){
470             this.showTooltip(this.getErrorText().join(''));
471         }
472         this.btns.saveBtn.setDisabled(!valid);
473         this.fireEvent('validation', this, valid);
474     },
475
476     lastVisibleColumn : function() {
477         var i = this.items.getCount() - 1,
478             c;
479         for(; i >= 0; i--) {
480             c = this.items.items[i];
481             if (!c.hidden) {
482                 return c;
483             }
484         }
485     },
486
487     showTooltip: function(msg){
488         var t = this.tooltip;
489         if(!t){
490             t = this.tooltip = new Ext.ToolTip({
491                 maxWidth: 600,
492                 cls: 'errorTip',
493                 width: 300,
494                 title: this.errorText,
495                 autoHide: false,
496                 anchor: 'left',
497                 anchorToTarget: true,
498                 mouseOffset: [40,0]
499             });
500         }
501         var v = this.grid.getView(),
502             top = parseInt(this.el.dom.style.top, 10),
503             scroll = v.scroller.dom.scrollTop,
504             h = this.el.getHeight();
505
506         if(top + h >= scroll){
507             t.initTarget(this.lastVisibleColumn().getEl());
508             if(!t.rendered){
509                 t.show();
510                 t.hide();
511             }
512             t.body.update(msg);
513             t.doAutoWidth(20);
514             t.show();
515         }else if(t.rendered){
516             t.hide();
517         }
518     },
519
520     getErrorText: function(){
521         var data = ['<ul>'];
522         this.items.each(function(f){
523             if(!f.isValid(true)){
524                 data.push('<li>', f.getActiveError(), '</li>');
525             }
526         });
527         data.push('</ul>');
528         return data;
529     }
530 });
531 Ext.preg('roweditor', Ext.ux.grid.RowEditor);