Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / docs / source / RowEditor.html
1 <!DOCTYPE html>
2 <html>
3 <head>
4   <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
5   <title>The source code</title>
6   <link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
7   <script type="text/javascript" src="../resources/prettify/prettify.js"></script>
8   <style type="text/css">
9     .highlight { display: block; background-color: #ddd; }
10   </style>
11   <script type="text/javascript">
12     function highlight() {
13       document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
14     }
15   </script>
16 </head>
17 <body onload="prettyPrint(); highlight();">
18   <pre class="prettyprint lang-js">// Currently has the following issues:
19 // - Does not handle postEditValue
20 // - Fields without editors need to sync with their values in Store
21 // - starting to edit another record while already editing and dirty should probably prevent it
22 // - aggregating validation messages
23 // - tabIndex is not managed bc we leave elements in dom, and simply move via positioning
24 // - layout issues when changing sizes/width while hidden (layout bug)
25
26 <span id='Ext-grid-RowEditor'>/**
27 </span> * @class Ext.grid.RowEditor
28  * @extends Ext.form.Panel
29  *
30  * Internal utility class used to provide row editing functionality. For developers, they should use
31  * the RowEditing plugin to use this functionality with a grid.
32  *
33  * @ignore
34  */
35 Ext.define('Ext.grid.RowEditor', {
36     extend: 'Ext.form.Panel',
37     requires: [
38         'Ext.tip.ToolTip',
39         'Ext.util.HashMap',
40         'Ext.util.KeyNav'
41     ],
42
43     saveBtnText  : 'Update',
44     cancelBtnText: 'Cancel',
45     errorsText: 'Errors',
46     dirtyText: 'You need to commit or cancel your changes',
47
48     lastScrollLeft: 0,
49     lastScrollTop: 0,
50
51     border: false,
52     
53     // Change the hideMode to offsets so that we get accurate measurements when
54     // the roweditor is hidden for laying out things like a TriggerField.
55     hideMode: 'offsets',
56
57     initComponent: function() {
58         var me = this,
59             form;
60
61         me.cls = Ext.baseCSSPrefix + 'grid-row-editor';
62
63         me.layout = {
64             type: 'hbox',
65             align: 'middle'
66         };
67
68         // Maintain field-to-column mapping
69         // It's easy to get a field from a column, but not vice versa
70         me.columns = Ext.create('Ext.util.HashMap');
71         me.columns.getKey = function(columnHeader) {
72             var f;
73             if (columnHeader.getEditor) {
74                 f = columnHeader.getEditor();
75                 if (f) {
76                     return f.id;
77                 }
78             }
79             return columnHeader.id;
80         };
81         me.mon(me.columns, {
82             add: me.onFieldAdd,
83             remove: me.onFieldRemove,
84             replace: me.onFieldReplace,
85             scope: me
86         });
87
88         me.callParent(arguments);
89
90         if (me.fields) {
91             me.setField(me.fields);
92             delete me.fields;
93         }
94
95         form = me.getForm();
96         form.trackResetOnLoad = true;
97     },
98
99     onFieldChange: function() {
100         var me = this,
101             form = me.getForm(),
102             valid = form.isValid();
103         if (me.errorSummary &amp;&amp; me.isVisible()) {
104             me[valid ? 'hideToolTip' : 'showToolTip']();
105         }
106         if (me.floatingButtons) {
107             me.floatingButtons.child('#update').setDisabled(!valid);
108         }
109         me.isValid = valid;
110     },
111
112     afterRender: function() {
113         var me = this,
114             plugin = me.editingPlugin;
115
116         me.callParent(arguments);
117         me.mon(me.renderTo, 'scroll', me.onCtScroll, me, { buffer: 100 });
118
119         // Prevent from bubbling click events to the grid view
120         me.mon(me.el, {
121             click: Ext.emptyFn,
122             stopPropagation: true
123         });
124
125         me.el.swallowEvent([
126             'keypress',
127             'keydown'
128         ]);
129
130         me.keyNav = Ext.create('Ext.util.KeyNav', me.el, {
131             enter: plugin.completeEdit,
132             esc: plugin.onEscKey,
133             scope: plugin
134         });
135
136         me.mon(plugin.view, {
137             beforerefresh: me.onBeforeViewRefresh,
138             refresh: me.onViewRefresh,
139             scope: me
140         });
141     },
142
143     onBeforeViewRefresh: function(view) {
144         var me = this,
145             viewDom = view.el.dom;
146
147         if (me.el.dom.parentNode === viewDom) {
148             viewDom.removeChild(me.el.dom);
149         }
150     },
151
152     onViewRefresh: function(view) {
153         var me = this,
154             viewDom = view.el.dom,
155             context = me.context,
156             idx;
157
158         viewDom.appendChild(me.el.dom);
159
160         // Recover our row node after a view refresh
161         if (context &amp;&amp; (idx = context.store.indexOf(context.record)) &gt;= 0) {
162             context.row = view.getNode(idx);
163             me.reposition();
164             if (me.tooltip &amp;&amp; me.tooltip.isVisible()) {
165                 me.tooltip.setTarget(context.row);
166             }
167         } else {
168             me.editingPlugin.cancelEdit();
169         }
170     },
171
172     onCtScroll: function(e, target) {
173         var me = this,
174             scrollTop  = target.scrollTop,
175             scrollLeft = target.scrollLeft;
176
177         if (scrollTop !== me.lastScrollTop) {
178             me.lastScrollTop = scrollTop;
179             if ((me.tooltip &amp;&amp; me.tooltip.isVisible()) || me.hiddenTip) {
180                 me.repositionTip();
181             }
182         }
183         if (scrollLeft !== me.lastScrollLeft) {
184             me.lastScrollLeft = scrollLeft;
185             me.reposition();
186         }
187     },
188
189     onColumnAdd: function(column) {
190         this.setField(column);
191     },
192
193     onColumnRemove: function(column) {
194         this.columns.remove(column);
195     },
196
197     onColumnResize: function(column, width) {
198         column.getEditor().setWidth(width - 2);
199         if (this.isVisible()) {
200             this.reposition();
201         }
202     },
203
204     onColumnHide: function(column) {
205         column.getEditor().hide();
206         if (this.isVisible()) {
207             this.reposition();
208         }
209     },
210
211     onColumnShow: function(column) {
212         var field = column.getEditor();
213         field.setWidth(column.getWidth() - 2).show();
214         if (this.isVisible()) {
215             this.reposition();
216         }
217     },
218
219     onColumnMove: function(column, fromIdx, toIdx) {
220         var field = column.getEditor();
221         if (this.items.indexOf(field) != toIdx) {
222             this.move(fromIdx, toIdx);
223         }
224     },
225
226     onFieldAdd: function(map, fieldId, column) {
227         var me = this,
228             colIdx = me.editingPlugin.grid.headerCt.getHeaderIndex(column),
229             field = column.getEditor({ xtype: 'displayfield' });
230
231         me.insert(colIdx, field);
232     },
233
234     onFieldRemove: function(map, fieldId, column) {
235         var me = this,
236             field = column.getEditor(),
237             fieldEl = field.el;
238         me.remove(field, false);
239         if (fieldEl) {
240             fieldEl.remove();
241         }
242     },
243
244     onFieldReplace: function(map, fieldId, column, oldColumn) {
245         var me = this;
246         me.onFieldRemove(map, fieldId, oldColumn);
247     },
248
249     clearFields: function() {
250         var me = this,
251             map = me.columns;
252         map.each(function(fieldId) {
253             map.removeAtKey(fieldId);
254         });
255     },
256
257     getFloatingButtons: function() {
258         var me = this,
259             cssPrefix = Ext.baseCSSPrefix,
260             btnsCss = cssPrefix + 'grid-row-editor-buttons',
261             plugin = me.editingPlugin,
262             btns;
263
264         if (!me.floatingButtons) {
265             btns = me.floatingButtons = Ext.create('Ext.Container', {
266                 renderTpl: [
267                     '&lt;div class=&quot;{baseCls}-ml&quot;&gt;&lt;/div&gt;',
268                     '&lt;div class=&quot;{baseCls}-mr&quot;&gt;&lt;/div&gt;',
269                     '&lt;div class=&quot;{baseCls}-bl&quot;&gt;&lt;/div&gt;',
270                     '&lt;div class=&quot;{baseCls}-br&quot;&gt;&lt;/div&gt;',
271                     '&lt;div class=&quot;{baseCls}-bc&quot;&gt;&lt;/div&gt;'
272                 ],
273
274                 renderTo: me.el,
275                 baseCls: btnsCss,
276                 layout: {
277                     type: 'hbox',
278                     align: 'middle'
279                 },
280                 defaults: {
281                     margins: '0 1 0 1'
282                 },
283                 items: [{
284                     itemId: 'update',
285                     flex: 1,
286                     xtype: 'button',
287                     handler: plugin.completeEdit,
288                     scope: plugin,
289                     text: me.saveBtnText,
290                     disabled: !me.isValid
291                 }, {
292                     flex: 1,
293                     xtype: 'button',
294                     handler: plugin.cancelEdit,
295                     scope: plugin,
296                     text: me.cancelBtnText
297                 }]
298             });
299
300             // Prevent from bubbling click events to the grid view
301             me.mon(btns.el, {
302                 // BrowserBug: Opera 11.01
303                 //   causes the view to scroll when a button is focused from mousedown
304                 mousedown: Ext.emptyFn,
305                 click: Ext.emptyFn,
306                 stopEvent: true
307             });
308         }
309         return me.floatingButtons;
310     },
311
312     reposition: function(animateConfig) {
313         var me = this,
314             context = me.context,
315             row = context &amp;&amp; Ext.get(context.row),
316             btns = me.getFloatingButtons(),
317             btnEl = btns.el,
318             grid = me.editingPlugin.grid,
319             viewEl = grid.view.el,
320             scroller = grid.verticalScroller,
321
322             // always get data from ColumnModel as its what drives
323             // the GridView's sizing
324             mainBodyWidth = grid.headerCt.getFullWidth(),
325             scrollerWidth = grid.getWidth(),
326
327             // use the minimum as the columns may not fill up the entire grid
328             // width
329             width = Math.min(mainBodyWidth, scrollerWidth),
330             scrollLeft = grid.view.el.dom.scrollLeft,
331             btnWidth = btns.getWidth(),
332             left = (width - btnWidth) / 2 + scrollLeft,
333             y, rowH, newHeight,
334
335             invalidateScroller = function() {
336                 if (scroller) {
337                     scroller.invalidate();
338                     btnEl.scrollIntoView(viewEl, false);
339                 }
340                 if (animateConfig &amp;&amp; animateConfig.callback) {
341                     animateConfig.callback.call(animateConfig.scope || me);
342                 }
343             };
344
345         // need to set both top/left
346         if (row &amp;&amp; Ext.isElement(row.dom)) {
347             // Bring our row into view if necessary, so a row editor that's already
348             // visible and animated to the row will appear smooth
349             row.scrollIntoView(viewEl, false);
350
351             // Get the y position of the row relative to its top-most static parent.
352             // offsetTop will be relative to the table, and is incorrect
353             // when mixed with certain grid features (e.g., grouping).
354             y = row.getXY()[1] - 5;
355             rowH = row.getHeight();
356             newHeight = rowH + 10;
357
358             // IE doesn't set the height quite right.
359             // This isn't a border-box issue, it even happens
360             // in IE8 and IE7 quirks.
361             // TODO: Test in IE9!
362             if (Ext.isIE) {
363                 newHeight += 2;
364             }
365
366             // Set editor height to match the row height
367             if (me.getHeight() != newHeight) {
368                 me.setHeight(newHeight);
369                 me.el.setLeft(0);
370             }
371
372             if (animateConfig) {
373                 var animObj = {
374                     to: {
375                         y: y
376                     },
377                     duration: animateConfig.duration || 125,
378                     listeners: {
379                         afteranimate: function() {
380                             invalidateScroller();
381                             y = row.getXY()[1] - 5;
382                             me.el.setY(y);
383                         }
384                     }
385                 };
386                 me.animate(animObj);
387             } else {
388                 me.el.setY(y);
389                 invalidateScroller();
390             }
391         }
392         if (me.getWidth() != mainBodyWidth) {
393             me.setWidth(mainBodyWidth);
394         }
395         btnEl.setLeft(left);
396     },
397
398     getEditor: function(fieldInfo) {
399         var me = this;
400
401         if (Ext.isNumber(fieldInfo)) {
402             // Query only form fields. This just future-proofs us in case we add
403             // other components to RowEditor later on.  Don't want to mess with
404             // indices.
405             return me.query('&gt;[isFormField]')[fieldInfo];
406         } else if (fieldInfo instanceof Ext.grid.column.Column) {
407             return fieldInfo.getEditor();
408         }
409     },
410
411     removeField: function(field) {
412         var me = this;
413
414         // Incase we pass a column instead, which is fine
415         field = me.getEditor(field);
416         me.mun(field, 'validitychange', me.onValidityChange, me);
417
418         // Remove field/column from our mapping, which will fire the event to
419         // remove the field from our container
420         me.columns.removeKey(field.id);
421     },
422
423     setField: function(column) {
424         var me = this,
425             field;
426
427         if (Ext.isArray(column)) {
428             Ext.Array.forEach(column, me.setField, me);
429             return;
430         }
431
432         // Get a default display field if necessary
433         field = column.getEditor(null, {
434             xtype: 'displayfield',
435             // Default display fields will not return values. This is done because
436             // the display field will pick up column renderers from the grid.
437             getModelData: function() {
438                 return null;
439             }
440         });
441         field.margins = '0 0 0 2';
442         field.setWidth(column.getDesiredWidth() - 2);
443         me.mon(field, 'change', me.onFieldChange, me);
444
445         // Maintain mapping of fields-to-columns
446         // This will fire events that maintain our container items
447         me.columns.add(field.id, column);
448         if (column.hidden) {
449             me.onColumnHide(column);
450         }
451         if (me.isVisible() &amp;&amp; me.context) {
452             me.renderColumnData(field, me.context.record);
453         }
454     },
455
456     loadRecord: function(record) {
457         var me = this,
458             form = me.getForm();
459         form.loadRecord(record);
460         if (form.isValid()) {
461             me.hideToolTip();
462         } else {
463             me.showToolTip();
464         }
465
466         // render display fields so they honor the column renderer/template
467         Ext.Array.forEach(me.query('&gt;displayfield'), function(field) {
468             me.renderColumnData(field, record);
469         }, me);
470     },
471
472     renderColumnData: function(field, record) {
473         var me = this,
474             grid = me.editingPlugin.grid,
475             headerCt = grid.headerCt,
476             view = grid.view,
477             store = view.store,
478             column = me.columns.get(field.id),
479             value = record.get(column.dataIndex);
480
481         // honor our column's renderer (TemplateHeader sets renderer for us!)
482         if (column.renderer) {
483             var metaData = { tdCls: '', style: '' },
484                 rowIdx = store.indexOf(record),
485                 colIdx = headerCt.getHeaderIndex(column);
486
487             value = column.renderer.call(
488                 column.scope || headerCt.ownerCt,
489                 value,
490                 metaData,
491                 record,
492                 rowIdx,
493                 colIdx,
494                 store,
495                 view
496             );
497         }
498
499         field.setRawValue(value);
500         field.resetOriginalValue();
501     },
502
503     beforeEdit: function() {
504         var me = this;
505
506         if (me.isVisible() &amp;&amp; !me.autoCancel &amp;&amp; me.isDirty()) {
507             me.showToolTip();
508             return false;
509         }
510     },
511
512 <span id='Ext-grid-RowEditor-method-startEdit'>    /**
513 </span>     * Start editing the specified grid at the specified position.
514      * @param {Ext.data.Model} record The Store data record which backs the row to be edited.
515      * @param {Ext.data.Model} columnHeader The Column object defining the column to be edited.
516      */
517     startEdit: function(record, columnHeader) {
518         var me = this,
519             grid = me.editingPlugin.grid,
520             view = grid.getView(),
521             store = grid.store,
522             context = me.context = Ext.apply(me.editingPlugin.context, {
523                 view: grid.getView(),
524                 store: store
525             });
526
527         // make sure our row is selected before editing
528         context.grid.getSelectionModel().select(record);
529
530         // Reload the record data
531         me.loadRecord(record);
532
533         if (!me.isVisible()) {
534             me.show();
535             me.focusContextCell();
536         } else {
537             me.reposition({
538                 callback: this.focusContextCell
539             });
540         }
541     },
542
543     // Focus the cell on start edit based upon the current context
544     focusContextCell: function() {
545         var field = this.getEditor(this.context.colIdx);
546         if (field &amp;&amp; field.focus) {
547             field.focus();
548         }
549     },
550
551     cancelEdit: function() {
552         var me = this,
553             form = me.getForm();
554
555         me.hide();
556         form.clearInvalid();
557         form.reset();
558     },
559
560     completeEdit: function() {
561         var me = this,
562             form = me.getForm();
563
564         if (!form.isValid()) {
565             return;
566         }
567
568         form.updateRecord(me.context.record);
569         me.hide();
570         return true;
571     },
572
573     onShow: function() {
574         var me = this;
575         me.callParent(arguments);
576         me.reposition();
577     },
578
579     onHide: function() {
580         var me = this;
581         me.callParent(arguments);
582         me.hideToolTip();
583         me.invalidateScroller();
584         if (me.context) {
585             me.context.view.focus();
586             me.context = null;
587         }
588     },
589
590     isDirty: function() {
591         var me = this,
592             form = me.getForm();
593         return form.isDirty();
594     },
595
596     getToolTip: function() {
597         var me = this,
598             tip;
599
600         if (!me.tooltip) {
601             tip = me.tooltip = Ext.createWidget('tooltip', {
602                 cls: Ext.baseCSSPrefix + 'grid-row-editor-errors',
603                 title: me.errorsText,
604                 autoHide: false,
605                 closable: true,
606                 closeAction: 'disable',
607                 anchor: 'left'
608             });
609         }
610         return me.tooltip;
611     },
612
613     hideToolTip: function() {
614         var me = this,
615             tip = me.getToolTip();
616         if (tip.rendered) {
617             tip.disable();
618         }
619         me.hiddenTip = false;
620     },
621
622     showToolTip: function() {
623         var me = this,
624             tip = me.getToolTip(),
625             context = me.context,
626             row = Ext.get(context.row),
627             viewEl = context.grid.view.el;
628
629         tip.setTarget(row);
630         tip.showAt([-10000, -10000]);
631         tip.body.update(me.getErrors());
632         tip.mouseOffset = [viewEl.getWidth() - row.getWidth() + me.lastScrollLeft + 15, 0];
633         me.repositionTip();
634         tip.doLayout();
635         tip.enable();
636     },
637
638     repositionTip: function() {
639         var me = this,
640             tip = me.getToolTip(),
641             context = me.context,
642             row = Ext.get(context.row),
643             viewEl = context.grid.view.el,
644             viewHeight = viewEl.getHeight(),
645             viewTop = me.lastScrollTop,
646             viewBottom = viewTop + viewHeight,
647             rowHeight = row.getHeight(),
648             rowTop = row.dom.offsetTop,
649             rowBottom = rowTop + rowHeight;
650
651         if (rowBottom &gt; viewTop &amp;&amp; rowTop &lt; viewBottom) {
652             tip.show();
653             me.hiddenTip = false;
654         } else {
655             tip.hide();
656             me.hiddenTip = true;
657         }
658     },
659
660     getErrors: function() {
661         var me = this,
662             dirtyText = !me.autoCancel &amp;&amp; me.isDirty() ? me.dirtyText + '&lt;br /&gt;' : '',
663             errors = [];
664
665         Ext.Array.forEach(me.query('&gt;[isFormField]'), function(field) {
666             errors = errors.concat(
667                 Ext.Array.map(field.getErrors(), function(e) {
668                     return '&lt;li&gt;' + e + '&lt;/li&gt;';
669                 })
670             );
671         }, me);
672
673         return dirtyText + '&lt;ul&gt;' + errors.join('') + '&lt;/ul&gt;';
674     },
675
676     invalidateScroller: function() {
677         var me = this,
678             context = me.context,
679             scroller = context.grid.verticalScroller;
680
681         if (scroller) {
682             scroller.invalidate();
683         }
684     }
685 });</pre>
686 </body>
687 </html>