Upgrade to ExtJS 3.0.3 - Released 10/11/2009
[extjs.git] / docs / source / RowEditor.html
1 <html>
2 <head>
3   <title>The source code</title>
4     <link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
5     <script type="text/javascript" src="../resources/prettify/prettify.js"></script>
6 </head>
7 <body  onload="prettyPrint();">
8     <pre class="prettyprint lang-js">Ext.ns('Ext.ux.grid');
9
10 <div id="cls-Ext.ux.grid.RowEditor"></div>/**
11  * @class Ext.ux.grid.RowEditor
12  * @extends Ext.Panel 
13  * Plugin (ptype = 'roweditor') that adds the ability to rapidly edit full rows in a grid.
14  * A validation mode may be enabled which uses AnchorTips to notify the user of all
15  * validation errors at once.
16  * 
17  * @ptype roweditor
18  */
19 Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, {
20     floating: true,
21     shadow: false,
22     layout: 'hbox',
23     cls: 'x-small-editor',
24     buttonAlign: 'center',
25     baseCls: 'x-row-editor',
26     elements: 'header,footer,body',
27     frameWidth: 5,
28     buttonPad: 3,
29     clicksToEdit: 'auto',
30     monitorValid: true,
31     focusDelay: 250,
32     errorSummary: true,
33     
34     saveText: 'Save',
35     cancelText: 'Cancel',
36     commitChangesText: 'You need to commit or cancel your changes',
37     errorText: 'Errors',
38
39     defaults: {
40         normalWidth: true
41     },
42
43     initComponent: function(){
44         Ext.ux.grid.RowEditor.superclass.initComponent.call(this);
45         this.addEvents(
46             <div id="event-Ext.ux.grid.RowEditor-beforeedit"></div>/**
47              * @event beforeedit
48              * Fired before the row editor is activated.
49              * If the listener returns <tt>false</tt> the editor will not be activated.
50              * @param {Ext.ux.grid.RowEditor} roweditor This object
51              * @param {Number} rowIndex The rowIndex of the row just edited
52              */
53             'beforeedit',
54             <div id="event-Ext.ux.grid.RowEditor-canceledit"></div>/**
55              * @event canceledit
56              * Fired when the editor is cancelled.
57              * @param {Ext.ux.grid.RowEditor} roweditor This object
58              * @param {Boolean} forced True if the cancel button is pressed, false is the editor was invalid. 
59              */
60             'canceledit',
61             <div id="event-Ext.ux.grid.RowEditor-validateedit"></div>/**
62              * @event validateedit
63              * Fired after a row is edited and passes validation.
64              * If the listener returns <tt>false</tt> changes to the record will not be set.
65              * @param {Ext.ux.grid.RowEditor} roweditor This object
66              * @param {Object} changes Object with changes made to the record.
67              * @param {Ext.data.Record} r The Record that was edited.
68              * @param {Number} rowIndex The rowIndex of the row just edited
69              */
70             'validateedit',
71             <div id="event-Ext.ux.grid.RowEditor-afteredit"></div>/**
72              * @event afteredit
73              * Fired after a row is edited and passes validation.  This event is fired
74              * after the store's update event is fired with this edit.
75              * @param {Ext.ux.grid.RowEditor} roweditor This object
76              * @param {Object} changes Object with changes made to the record.
77              * @param {Ext.data.Record} r The Record that was edited.
78              * @param {Number} rowIndex The rowIndex of the row just edited
79              */
80             'afteredit'
81         );
82     },
83
84     init: function(grid){
85         this.grid = grid;
86         this.ownerCt = grid;
87         if(this.clicksToEdit === 2){
88             grid.on('rowdblclick', this.onRowDblClick, this);
89         }else{
90             grid.on('rowclick', this.onRowClick, this);
91             if(Ext.isIE){
92                 grid.on('rowdblclick', this.onRowDblClick, this);
93             }
94         }
95
96         // stopEditing without saving when a record is removed from Store.
97         grid.getStore().on('remove', function() {
98             this.stopEditing(false);
99         },this);
100
101         grid.on({
102             scope: this,
103             keydown: this.onGridKey,
104             columnresize: this.verifyLayout,
105             columnmove: this.refreshFields,
106             reconfigure: this.refreshFields,
107             beforedestroy : this.beforedestroy,
108             destroy : this.destroy,
109             bodyscroll: {
110                 buffer: 250,
111                 fn: this.positionButtons
112             }
113         });
114         grid.getColumnModel().on('hiddenchange', this.verifyLayout, this, {delay:1});
115         grid.getView().on('refresh', this.stopEditing.createDelegate(this, []));
116     },
117
118     beforedestroy: function() {
119         this.grid.getStore().un('remove', this.onStoreRemove, this);
120         this.stopEditing(false);
121         Ext.destroy(this.btns);
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.fly(row).getHeight() + 9);
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                 ed.margins = pm('0 1 2');
269             }
270             ed.setWidth(cm.getColumnWidth(i));
271             ed.column = c;
272             if(ed.ownerCt !== this){
273                 ed.on('focus', this.ensureVisible, this);
274                 ed.on('specialkey', this.onKey, this);
275             }
276             this.insert(i, ed);
277         }
278         this.initialized = true;
279     },
280
281     onKey: function(f, e){
282         if(e.getKey() === e.ENTER){
283             this.stopEditing(true);
284             e.stopPropagation();
285         }
286     },
287
288     onGridKey: function(e){
289         if(e.getKey() === e.ENTER && !this.isVisible()){
290             var r = this.grid.getSelectionModel().getSelected();
291             if(r){
292                 var index = this.grid.store.indexOf(r);
293                 this.startEditing(index);
294                 e.stopPropagation();
295             }
296         }
297     },
298
299     ensureVisible: function(editor){
300         if(this.isVisible()){
301              this.grid.getView().ensureVisible(this.rowIndex, this.grid.colModel.getIndexById(editor.column.id), true);
302         }
303     },
304
305     onRowClick: function(g, rowIndex, e){
306         if(this.clicksToEdit == 'auto'){
307             var li = this.lastClickIndex;
308             this.lastClickIndex = rowIndex;
309             if(li != rowIndex && !this.isVisible()){
310                 return;
311             }
312         }
313         this.startEditing(rowIndex, false);
314         this.doFocus.defer(this.focusDelay, this, [e.getPoint()]);
315     },
316
317     onRowDblClick: function(g, rowIndex, e){
318         this.startEditing(rowIndex, false);
319         this.doFocus.defer(this.focusDelay, this, [e.getPoint()]);
320     },
321
322     onRender: function(){
323         Ext.ux.grid.RowEditor.superclass.onRender.apply(this, arguments);
324         this.el.swallowEvent(['keydown', 'keyup', 'keypress']);
325         this.btns = new Ext.Panel({
326             baseCls: 'x-plain',
327             cls: 'x-btns',
328             elements:'body',
329             layout: 'table',
330             width: (this.minButtonWidth * 2) + (this.frameWidth * 2) + (this.buttonPad * 4), // width must be specified for IE
331             items: [{
332                 ref: 'saveBtn',
333                 itemId: 'saveBtn',
334                 xtype: 'button',
335                 text: this.saveText,
336                 width: this.minButtonWidth,
337                 handler: this.stopEditing.createDelegate(this, [true])
338             }, {
339                 xtype: 'button',
340                 text: this.cancelText,
341                 width: this.minButtonWidth,
342                 handler: this.stopEditing.createDelegate(this, [false])
343             }]
344         });
345         this.btns.render(this.bwrap);
346     },
347
348     afterRender: function(){
349         Ext.ux.grid.RowEditor.superclass.afterRender.apply(this, arguments);
350         this.positionButtons();
351         if(this.monitorValid){
352             this.startMonitoring();
353         }
354     },
355
356     onShow: function(){
357         if(this.monitorValid){
358             this.startMonitoring();
359         }
360         Ext.ux.grid.RowEditor.superclass.onShow.apply(this, arguments);
361     },
362
363     onHide: function(){
364         Ext.ux.grid.RowEditor.superclass.onHide.apply(this, arguments);
365         this.stopMonitoring();
366         this.grid.getView().focusRow(this.rowIndex);
367     },
368
369     positionButtons: function(){
370         if(this.btns){
371             var g = this.grid,
372                 h = this.el.dom.clientHeight,
373                 view = g.getView(),
374                 scroll = view.scroller.dom.scrollLeft,
375                 bw = this.btns.getWidth(),
376                 width = Math.min(g.getWidth(), g.getColumnModel().getTotalWidth());
377                 
378             this.btns.el.shift({left: (width/2)-(bw/2)+scroll, top: h - 2, stopFx: true, duration:0.2});
379         }
380     },
381
382     // private
383     preEditValue : function(r, field){
384         var value = r.data[field];
385         return this.autoEncode && typeof value === 'string' ? Ext.util.Format.htmlDecode(value) : value;
386     },
387
388     // private
389     postEditValue : function(value, originalValue, r, field){
390         return this.autoEncode && typeof value == 'string' ? Ext.util.Format.htmlEncode(value) : value;
391     },
392
393     doFocus: function(pt){
394         if(this.isVisible()){
395             var index = 0,
396                 cm = this.grid.getColumnModel(),
397                 c;
398             if(pt){
399                 index = this.getTargetColumnIndex(pt);
400             }
401             for(var i = index||0, len = cm.getColumnCount(); i < len; i++){
402                 c = cm.getColumnAt(i);
403                 if(!c.hidden && c.getEditor()){
404                     c.getEditor().focus();
405                     break;
406                 }
407             }
408         }
409     },
410
411     getTargetColumnIndex: function(pt){
412         var grid = this.grid, 
413             v = grid.view,
414             x = pt.left,
415             cms = grid.colModel.config,
416             i = 0, 
417             match = false;
418         for(var len = cms.length, c; c = cms[i]; i++){
419             if(!c.hidden){
420                 if(Ext.fly(v.getHeaderCell(i)).getRegion().right >= x){
421                     match = i;
422                     break;
423                 }
424             }
425         }
426         return match;
427     },
428
429     startMonitoring : function(){
430         if(!this.bound && this.monitorValid){
431             this.bound = true;
432             Ext.TaskMgr.start({
433                 run : this.bindHandler,
434                 interval : this.monitorPoll || 200,
435                 scope: this
436             });
437         }
438     },
439
440     stopMonitoring : function(){
441         this.bound = false;
442         if(this.tooltip){
443             this.tooltip.hide();
444         }
445     },
446
447     isValid: function(){
448         var valid = true;
449         this.items.each(function(f){
450             if(!f.isValid(true)){
451                 valid = false;
452                 return false;
453             }
454         });
455         return valid;
456     },
457
458     // private
459     bindHandler : function(){
460         if(!this.bound){
461             return false; // stops binding
462         }
463         var valid = this.isValid();
464         if(!valid && this.errorSummary){
465             this.showTooltip(this.getErrorText().join(''));
466         }
467         this.btns.saveBtn.setDisabled(!valid);
468         this.fireEvent('validation', this, valid);
469     },
470
471     showTooltip: function(msg){
472         var t = this.tooltip;
473         if(!t){
474             t = this.tooltip = new Ext.ToolTip({
475                 maxWidth: 600,
476                 cls: 'errorTip',
477                 width: 300,
478                 title: this.errorText,
479                 autoHide: false,
480                 anchor: 'left',
481                 anchorToTarget: true,
482                 mouseOffset: [40,0]
483             });
484         }
485         var v = this.grid.getView(),
486             top = parseInt(this.el.dom.style.top, 10),
487             scroll = v.scroller.dom.scrollTop,
488             h = this.el.getHeight();
489                 
490         if(top + h >= scroll){
491             t.initTarget(this.items.last().getEl());
492             if(!t.rendered){
493                 t.show();
494                 t.hide();
495             }
496             t.body.update(msg);
497             t.doAutoWidth();
498             t.show();
499         }else if(t.rendered){
500             t.hide();
501         }
502     },
503
504     getErrorText: function(){
505         var data = ['<ul>'];
506         this.items.each(function(f){
507             if(!f.isValid(true)){
508                 data.push('<li>', f.activeError, '</li>');
509             }
510         });
511         data.push('</ul>');
512         return data;
513     }
514 });
515 Ext.preg('roweditor', Ext.ux.grid.RowEditor);
516
517 Ext.override(Ext.form.Field, {
518     markInvalid : function(msg){
519         if(!this.rendered || this.preventMark){ // not rendered
520             return;
521         }
522         msg = msg || this.invalidText;
523
524         var mt = this.getMessageHandler();
525         if(mt){
526             mt.mark(this, msg);
527         }else if(this.msgTarget){
528             this.el.addClass(this.invalidClass);
529             var t = Ext.getDom(this.msgTarget);
530             if(t){
531                 t.innerHTML = msg;
532                 t.style.display = this.msgDisplay;
533             }
534         }
535         this.activeError = msg;
536         this.fireEvent('invalid', this, msg);
537     }
538 });
539
540 Ext.override(Ext.ToolTip, {
541     doAutoWidth : function(){
542         var bw = this.body.getTextWidth();
543         if(this.title){
544             bw = Math.max(bw, this.header.child('span').getTextWidth(this.title));
545         }
546         bw += this.getFrameWidth() + (this.closable ? 20 : 0) + this.body.getPadding("lr") + 20;
547         this.setWidth(bw.constrain(this.minWidth, this.maxWidth));
548
549         // IE7 repaint bug on initial show
550         if(Ext.isIE7 && !this.repainted){
551             this.el.repaint();
552             this.repainted = true;
553         }
554     }
555 });
556 </pre>
557 </body>
558 </html>