Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / docs / source / RowEditor.html
index 7e158b4..f73b0d2 100644 (file)
+<!DOCTYPE html>
 <html>
 <head>
+  <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
   <title>The source code</title>
-    <link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
-    <script type="text/javascript" src="../resources/prettify/prettify.js"></script>
+  <link href="../resources/prettify/prettify.css" type="text/css" rel="stylesheet" />
+  <script type="text/javascript" src="../resources/prettify/prettify.js"></script>
+  <style type="text/css">
+    .highlight { display: block; background-color: #ddd; }
+  </style>
+  <script type="text/javascript">
+    function highlight() {
+      document.getElementById(location.hash.replace(/#/, "")).className = "highlight";
+    }
+  </script>
 </head>
-<body  onload="prettyPrint();">
-    <pre class="prettyprint lang-js">Ext.ns('Ext.ux.grid');
-
-<div id="cls-Ext.ux.grid.RowEditor"></div>/**
- * @class Ext.ux.grid.RowEditor
- * @extends Ext.Panel 
- * Plugin (ptype = 'roweditor') that adds the ability to rapidly edit full rows in a grid.
- * A validation mode may be enabled which uses AnchorTips to notify the user of all
- * validation errors at once.
- * 
- * @ptype roweditor
+<body onload="prettyPrint(); highlight();">
+  <pre class="prettyprint lang-js">// Currently has the following issues:
+// - Does not handle postEditValue
+// - Fields without editors need to sync with their values in Store
+// - starting to edit another record while already editing and dirty should probably prevent it
+// - aggregating validation messages
+// - tabIndex is not managed bc we leave elements in dom, and simply move via positioning
+// - layout issues when changing sizes/width while hidden (layout bug)
+
+<span id='Ext-grid-RowEditor'>/**
+</span> * @class Ext.grid.RowEditor
+ * @extends Ext.form.Panel
+ *
+ * Internal utility class used to provide row editing functionality. For developers, they should use
+ * the RowEditing plugin to use this functionality with a grid.
+ *
+ * @ignore
  */
-Ext.ux.grid.RowEditor = Ext.extend(Ext.Panel, {
-    floating: true,
-    shadow: false,
-    layout: 'hbox',
-    cls: 'x-small-editor',
-    buttonAlign: 'center',
-    baseCls: 'x-row-editor',
-    elements: 'header,footer,body',
-    frameWidth: 5,
-    buttonPad: 3,
-    clicksToEdit: 'auto',
-    monitorValid: true,
-    focusDelay: 250,
-    errorSummary: true,
+Ext.define('Ext.grid.RowEditor', {
+    extend: 'Ext.form.Panel',
+    requires: [
+        'Ext.tip.ToolTip',
+        'Ext.util.HashMap',
+        'Ext.util.KeyNav'
+    ],
+
+    saveBtnText  : 'Update',
+    cancelBtnText: 'Cancel',
+    errorsText: 'Errors',
+    dirtyText: 'You need to commit or cancel your changes',
+
+    lastScrollLeft: 0,
+    lastScrollTop: 0,
+
+    border: false,
     
-    saveText: 'Save',
-    cancelText: 'Cancel',
-    commitChangesText: 'You need to commit or cancel your changes',
-    errorText: 'Errors',
-
-    defaults: {
-        normalWidth: true
-    },
-
-    initComponent: function(){
-        Ext.ux.grid.RowEditor.superclass.initComponent.call(this);
-        this.addEvents(
-            <div id="event-Ext.ux.grid.RowEditor-beforeedit"></div>/**
-             * @event beforeedit
-             * Fired before the row editor is activated.
-             * If the listener returns <tt>false</tt> the editor will not be activated.
-             * @param {Ext.ux.grid.RowEditor} roweditor This object
-             * @param {Number} rowIndex The rowIndex of the row just edited
-             */
-            'beforeedit',
-            <div id="event-Ext.ux.grid.RowEditor-canceledit"></div>/**
-             * @event canceledit
-             * Fired when the editor is cancelled.
-             * @param {Ext.ux.grid.RowEditor} roweditor This object
-             * @param {Boolean} forced True if the cancel button is pressed, false is the editor was invalid. 
-             */
-            'canceledit',
-            <div id="event-Ext.ux.grid.RowEditor-validateedit"></div>/**
-             * @event validateedit
-             * Fired after a row is edited and passes validation.
-             * If the listener returns <tt>false</tt> changes to the record will not be set.
-             * @param {Ext.ux.grid.RowEditor} roweditor This object
-             * @param {Object} changes Object with changes made to the record.
-             * @param {Ext.data.Record} r The Record that was edited.
-             * @param {Number} rowIndex The rowIndex of the row just edited
-             */
-            'validateedit',
-            <div id="event-Ext.ux.grid.RowEditor-afteredit"></div>/**
-             * @event afteredit
-             * Fired after a row is edited and passes validation.  This event is fired
-             * after the store's update event is fired with this edit.
-             * @param {Ext.ux.grid.RowEditor} roweditor This object
-             * @param {Object} changes Object with changes made to the record.
-             * @param {Ext.data.Record} r The Record that was edited.
-             * @param {Number} rowIndex The rowIndex of the row just edited
-             */
-            'afteredit'
-        );
-    },
-
-    init: function(grid){
-        this.grid = grid;
-        this.ownerCt = grid;
-        if(this.clicksToEdit === 2){
-            grid.on('rowdblclick', this.onRowDblClick, this);
-        }else{
-            grid.on('rowclick', this.onRowClick, this);
-            if(Ext.isIE){
-                grid.on('rowdblclick', this.onRowDblClick, this);
+    // Change the hideMode to offsets so that we get accurate measurements when
+    // the roweditor is hidden for laying out things like a TriggerField.
+    hideMode: 'offsets',
+
+    initComponent: function() {
+        var me = this,
+            form;
+
+        me.cls = Ext.baseCSSPrefix + 'grid-row-editor';
+
+        me.layout = {
+            type: 'hbox',
+            align: 'middle'
+        };
+
+        // Maintain field-to-column mapping
+        // It's easy to get a field from a column, but not vice versa
+        me.columns = Ext.create('Ext.util.HashMap');
+        me.columns.getKey = function(columnHeader) {
+            var f;
+            if (columnHeader.getEditor) {
+                f = columnHeader.getEditor();
+                if (f) {
+                    return f.id;
+                }
             }
+            return columnHeader.id;
+        };
+        me.mon(me.columns, {
+            add: me.onFieldAdd,
+            remove: me.onFieldRemove,
+            replace: me.onFieldReplace,
+            scope: me
+        });
+
+        me.callParent(arguments);
+
+        if (me.fields) {
+            me.setField(me.fields);
+            delete me.fields;
         }
 
-        // stopEditing without saving when a record is removed from Store.
-        grid.getStore().on('remove', function() {
-            this.stopEditing(false);
-        },this);
-
-        grid.on({
-            scope: this,
-            keydown: this.onGridKey,
-            columnresize: this.verifyLayout,
-            columnmove: this.refreshFields,
-            reconfigure: this.refreshFields,
-            beforedestroy : this.beforedestroy,
-            destroy : this.destroy,
-            bodyscroll: {
-                buffer: 250,
-                fn: this.positionButtons
-            }
-        });
-        grid.getColumnModel().on('hiddenchange', this.verifyLayout, this, {delay:1});
-        grid.getView().on('refresh', this.stopEditing.createDelegate(this, []));
+        form = me.getForm();
+        form.trackResetOnLoad = true;
     },
 
-    beforedestroy: function() {
-        this.grid.getStore().un('remove', this.onStoreRemove, this);
-        this.stopEditing(false);
-        Ext.destroy(this.btns);
+    onFieldChange: function() {
+        var me = this,
+            form = me.getForm(),
+            valid = form.isValid();
+        if (me.errorSummary &amp;&amp; me.isVisible()) {
+            me[valid ? 'hideToolTip' : 'showToolTip']();
+        }
+        if (me.floatingButtons) {
+            me.floatingButtons.child('#update').setDisabled(!valid);
+        }
+        me.isValid = valid;
     },
 
-    refreshFields: function(){
-        this.initFields();
-        this.verifyLayout();
-    },
+    afterRender: function() {
+        var me = this,
+            plugin = me.editingPlugin;
 
-    isDirty: function(){
-        var dirty;
-        this.items.each(function(f){
-            if(String(this.values[f.id]) !== String(f.getValue())){
-                dirty = true;
-                return false;
-            }
-        }, this);
-        return dirty;
+        me.callParent(arguments);
+        me.mon(me.renderTo, 'scroll', me.onCtScroll, me, { buffer: 100 });
+
+        // Prevent from bubbling click events to the grid view
+        me.mon(me.el, {
+            click: Ext.emptyFn,
+            stopPropagation: true
+        });
+
+        me.el.swallowEvent([
+            'keypress',
+            'keydown'
+        ]);
+
+        me.keyNav = Ext.create('Ext.util.KeyNav', me.el, {
+            enter: plugin.completeEdit,
+            esc: plugin.onEscKey,
+            scope: plugin
+        });
+
+        me.mon(plugin.view, {
+            beforerefresh: me.onBeforeViewRefresh,
+            refresh: me.onViewRefresh,
+            scope: me
+        });
     },
 
-    startEditing: function(rowIndex, doFocus){
-        if(this.editing && this.isDirty()){
-            this.showTooltip(this.commitChangesText);
-            return;
-        }
-        if(Ext.isObject(rowIndex)){
-            rowIndex = this.grid.getStore().indexOf(rowIndex);
+    onBeforeViewRefresh: function(view) {
+        var me = this,
+            viewDom = view.el.dom;
+
+        if (me.el.dom.parentNode === viewDom) {
+            viewDom.removeChild(me.el.dom);
         }
-        if(this.fireEvent('beforeedit', this, rowIndex) !== false){
-            this.editing = true;
-            var g = this.grid, view = g.getView(),
-                row = view.getRow(rowIndex),
-                record = g.store.getAt(rowIndex);
-                
-            this.record = record;
-            this.rowIndex = rowIndex;
-            this.values = {};
-            if(!this.rendered){
-                this.render(view.getEditorParent());
-            }
-            var w = Ext.fly(row).getWidth();
-            this.setSize(w);
-            if(!this.initialized){
-                this.initFields();
-            }
-            var cm = g.getColumnModel(), fields = this.items.items, f, val;
-            for(var i = 0, len = cm.getColumnCount(); i < len; i++){
-                val = this.preEditValue(record, cm.getDataIndex(i));
-                f = fields[i];
-                f.setValue(val);
-                this.values[f.id] = Ext.isEmpty(val) ? '' : val;
-            }
-            this.verifyLayout(true);
-            if(!this.isVisible()){
-                this.setPagePosition(Ext.fly(row).getXY());
-            } else{
-                this.el.setXY(Ext.fly(row).getXY(), {duration:0.15});
-            }
-            if(!this.isVisible()){
-                this.show().doLayout();
-            }
-            if(doFocus !== false){
-                this.doFocus.defer(this.focusDelay, this);
+    },
+
+    onViewRefresh: function(view) {
+        var me = this,
+            viewDom = view.el.dom,
+            context = me.context,
+            idx;
+
+        viewDom.appendChild(me.el.dom);
+
+        // Recover our row node after a view refresh
+        if (context &amp;&amp; (idx = context.store.indexOf(context.record)) &gt;= 0) {
+            context.row = view.getNode(idx);
+            me.reposition();
+            if (me.tooltip &amp;&amp; me.tooltip.isVisible()) {
+                me.tooltip.setTarget(context.row);
             }
+        } else {
+            me.editingPlugin.cancelEdit();
         }
     },
 
-    stopEditing : function(saveChanges){
-        this.editing = false;
-        if(!this.isVisible()){
-            return;
-        }
-        if(saveChanges === false || !this.isValid()){
-            this.hide();
-            this.fireEvent('canceledit', this, saveChanges === false);
-            return;
-        }
-        var changes = {}, 
-            r = this.record, 
-            hasChange = false,
-            cm = this.grid.colModel, 
-            fields = this.items.items;
-        for(var i = 0, len = cm.getColumnCount(); i < len; i++){
-            if(!cm.isHidden(i)){
-                var dindex = cm.getDataIndex(i);
-                if(!Ext.isEmpty(dindex)){
-                    var oldValue = r.data[dindex],
-                        value = this.postEditValue(fields[i].getValue(), oldValue, r, dindex);
-                    if(String(oldValue) !== String(value)){
-                        changes[dindex] = value;
-                        hasChange = true;
-                    }
-                }
+    onCtScroll: function(e, target) {
+        var me = this,
+            scrollTop  = target.scrollTop,
+            scrollLeft = target.scrollLeft;
+
+        if (scrollTop !== me.lastScrollTop) {
+            me.lastScrollTop = scrollTop;
+            if ((me.tooltip &amp;&amp; me.tooltip.isVisible()) || me.hiddenTip) {
+                me.repositionTip();
             }
         }
-        if(hasChange && this.fireEvent('validateedit', this, changes, r, this.rowIndex) !== false){
-            r.beginEdit();
-            Ext.iterate(changes, function(name, value){
-                r.set(name, value);
-            });
-            r.endEdit();
-            this.fireEvent('afteredit', this, changes, r, this.rowIndex);
-        }
-        this.hide();
-    },
-
-    verifyLayout: function(force){
-        if(this.el && (this.isVisible() || force === true)){
-            var row = this.grid.getView().getRow(this.rowIndex);
-            this.setSize(Ext.fly(row).getWidth(), Ext.fly(row).getHeight() + 9);
-            var cm = this.grid.colModel, fields = this.items.items;
-            for(var i = 0, len = cm.getColumnCount(); i < len; i++){
-                if(!cm.isHidden(i)){
-                    var adjust = 0;
-                    if(i === (len - 1)){
-                        adjust += 3; // outer padding
-                    } else{
-                        adjust += 1;
-                    }
-                    fields[i].show();
-                    fields[i].setWidth(cm.getColumnWidth(i) - adjust);
-                } else{
-                    fields[i].hide();
-                }
-            }
-            this.doLayout();
-            this.positionButtons();
+        if (scrollLeft !== me.lastScrollLeft) {
+            me.lastScrollLeft = scrollLeft;
+            me.reposition();
         }
     },
 
-    slideHide : function(){
-        this.hide();
+    onColumnAdd: function(column) {
+        this.setField(column);
     },
 
-    initFields: function(){
-        var cm = this.grid.getColumnModel(), pm = Ext.layout.ContainerLayout.prototype.parseMargins;
-        this.removeAll(false);
-        for(var i = 0, len = cm.getColumnCount(); i < len; i++){
-            var c = cm.getColumnAt(i),
-                ed = c.getEditor();
-            if(!ed){
-                ed = c.displayEditor || new Ext.form.DisplayField();
-            }
-            if(i == 0){
-                ed.margins = pm('0 1 2 1');
-            } else if(i == len - 1){
-                ed.margins = pm('0 0 2 1');
-            } else{
-                ed.margins = pm('0 1 2');
-            }
-            ed.setWidth(cm.getColumnWidth(i));
-            ed.column = c;
-            if(ed.ownerCt !== this){
-                ed.on('focus', this.ensureVisible, this);
-                ed.on('specialkey', this.onKey, this);
-            }
-            this.insert(i, ed);
+    onColumnRemove: function(column) {
+        this.columns.remove(column);
+    },
+
+    onColumnResize: function(column, width) {
+        column.getEditor().setWidth(width - 2);
+        if (this.isVisible()) {
+            this.reposition();
         }
-        this.initialized = true;
     },
 
-    onKey: function(f, e){
-        if(e.getKey() === e.ENTER){
-            this.stopEditing(true);
-            e.stopPropagation();
+    onColumnHide: function(column) {
+        column.getEditor().hide();
+        if (this.isVisible()) {
+            this.reposition();
         }
     },
 
-    onGridKey: function(e){
-        if(e.getKey() === e.ENTER && !this.isVisible()){
-            var r = this.grid.getSelectionModel().getSelected();
-            if(r){
-                var index = this.grid.store.indexOf(r);
-                this.startEditing(index);
-                e.stopPropagation();
-            }
+    onColumnShow: function(column) {
+        var field = column.getEditor();
+        field.setWidth(column.getWidth() - 2).show();
+        if (this.isVisible()) {
+            this.reposition();
         }
     },
 
-    ensureVisible: function(editor){
-        if(this.isVisible()){
-             this.grid.getView().ensureVisible(this.rowIndex, this.grid.colModel.getIndexById(editor.column.id), true);
+    onColumnMove: function(column, fromIdx, toIdx) {
+        var field = column.getEditor();
+        if (this.items.indexOf(field) != toIdx) {
+            this.move(fromIdx, toIdx);
         }
     },
 
-    onRowClick: function(g, rowIndex, e){
-        if(this.clicksToEdit == 'auto'){
-            var li = this.lastClickIndex;
-            this.lastClickIndex = rowIndex;
-            if(li != rowIndex && !this.isVisible()){
-                return;
-            }
+    onFieldAdd: function(map, fieldId, column) {
+        var me = this,
+            colIdx = me.editingPlugin.grid.headerCt.getHeaderIndex(column),
+            field = column.getEditor({ xtype: 'displayfield' });
+
+        me.insert(colIdx, field);
+    },
+
+    onFieldRemove: function(map, fieldId, column) {
+        var me = this,
+            field = column.getEditor(),
+            fieldEl = field.el;
+        me.remove(field, false);
+        if (fieldEl) {
+            fieldEl.remove();
         }
-        this.startEditing(rowIndex, false);
-        this.doFocus.defer(this.focusDelay, this, [e.getPoint()]);
-    },
-
-    onRowDblClick: function(g, rowIndex, e){
-        this.startEditing(rowIndex, false);
-        this.doFocus.defer(this.focusDelay, this, [e.getPoint()]);
-    },
-
-    onRender: function(){
-        Ext.ux.grid.RowEditor.superclass.onRender.apply(this, arguments);
-        this.el.swallowEvent(['keydown', 'keyup', 'keypress']);
-        this.btns = new Ext.Panel({
-            baseCls: 'x-plain',
-            cls: 'x-btns',
-            elements:'body',
-            layout: 'table',
-            width: (this.minButtonWidth * 2) + (this.frameWidth * 2) + (this.buttonPad * 4), // width must be specified for IE
-            items: [{
-                ref: 'saveBtn',
-                itemId: 'saveBtn',
-                xtype: 'button',
-                text: this.saveText,
-                width: this.minButtonWidth,
-                handler: this.stopEditing.createDelegate(this, [true])
-            }, {
-                xtype: 'button',
-                text: this.cancelText,
-                width: this.minButtonWidth,
-                handler: this.stopEditing.createDelegate(this, [false])
-            }]
+    },
+
+    onFieldReplace: function(map, fieldId, column, oldColumn) {
+        var me = this;
+        me.onFieldRemove(map, fieldId, oldColumn);
+    },
+
+    clearFields: function() {
+        var me = this,
+            map = me.columns;
+        map.each(function(fieldId) {
+            map.removeAtKey(fieldId);
         });
-        this.btns.render(this.bwrap);
     },
 
-    afterRender: function(){
-        Ext.ux.grid.RowEditor.superclass.afterRender.apply(this, arguments);
-        this.positionButtons();
-        if(this.monitorValid){
-            this.startMonitoring();
+    getFloatingButtons: function() {
+        var me = this,
+            cssPrefix = Ext.baseCSSPrefix,
+            btnsCss = cssPrefix + 'grid-row-editor-buttons',
+            plugin = me.editingPlugin,
+            btns;
+
+        if (!me.floatingButtons) {
+            btns = me.floatingButtons = Ext.create('Ext.Container', {
+                renderTpl: [
+                    '&lt;div class=&quot;{baseCls}-ml&quot;&gt;&lt;/div&gt;',
+                    '&lt;div class=&quot;{baseCls}-mr&quot;&gt;&lt;/div&gt;',
+                    '&lt;div class=&quot;{baseCls}-bl&quot;&gt;&lt;/div&gt;',
+                    '&lt;div class=&quot;{baseCls}-br&quot;&gt;&lt;/div&gt;',
+                    '&lt;div class=&quot;{baseCls}-bc&quot;&gt;&lt;/div&gt;'
+                ],
+
+                renderTo: me.el,
+                baseCls: btnsCss,
+                layout: {
+                    type: 'hbox',
+                    align: 'middle'
+                },
+                defaults: {
+                    margins: '0 1 0 1'
+                },
+                items: [{
+                    itemId: 'update',
+                    flex: 1,
+                    xtype: 'button',
+                    handler: plugin.completeEdit,
+                    scope: plugin,
+                    text: me.saveBtnText,
+                    disabled: !me.isValid
+                }, {
+                    flex: 1,
+                    xtype: 'button',
+                    handler: plugin.cancelEdit,
+                    scope: plugin,
+                    text: me.cancelBtnText
+                }]
+            });
+
+            // Prevent from bubbling click events to the grid view
+            me.mon(btns.el, {
+                // BrowserBug: Opera 11.01
+                //   causes the view to scroll when a button is focused from mousedown
+                mousedown: Ext.emptyFn,
+                click: Ext.emptyFn,
+                stopEvent: true
+            });
+        }
+        return me.floatingButtons;
+    },
+
+    reposition: function(animateConfig) {
+        var me = this,
+            context = me.context,
+            row = context &amp;&amp; Ext.get(context.row),
+            btns = me.getFloatingButtons(),
+            btnEl = btns.el,
+            grid = me.editingPlugin.grid,
+            viewEl = grid.view.el,
+            scroller = grid.verticalScroller,
+
+            // always get data from ColumnModel as its what drives
+            // the GridView's sizing
+            mainBodyWidth = grid.headerCt.getFullWidth(),
+            scrollerWidth = grid.getWidth(),
+
+            // use the minimum as the columns may not fill up the entire grid
+            // width
+            width = Math.min(mainBodyWidth, scrollerWidth),
+            scrollLeft = grid.view.el.dom.scrollLeft,
+            btnWidth = btns.getWidth(),
+            left = (width - btnWidth) / 2 + scrollLeft,
+            y, rowH, newHeight,
+
+            invalidateScroller = function() {
+                if (scroller) {
+                    scroller.invalidate();
+                    btnEl.scrollIntoView(viewEl, false);
+                }
+                if (animateConfig &amp;&amp; animateConfig.callback) {
+                    animateConfig.callback.call(animateConfig.scope || me);
+                }
+            };
+
+        // need to set both top/left
+        if (row &amp;&amp; Ext.isElement(row.dom)) {
+            // Bring our row into view if necessary, so a row editor that's already
+            // visible and animated to the row will appear smooth
+            row.scrollIntoView(viewEl, false);
+
+            // Get the y position of the row relative to its top-most static parent.
+            // offsetTop will be relative to the table, and is incorrect
+            // when mixed with certain grid features (e.g., grouping).
+            y = row.getXY()[1] - 5;
+            rowH = row.getHeight();
+            newHeight = rowH + 10;
+
+            // IE doesn't set the height quite right.
+            // This isn't a border-box issue, it even happens
+            // in IE8 and IE7 quirks.
+            // TODO: Test in IE9!
+            if (Ext.isIE) {
+                newHeight += 2;
+            }
+
+            // Set editor height to match the row height
+            if (me.getHeight() != newHeight) {
+                me.setHeight(newHeight);
+                me.el.setLeft(0);
+            }
+
+            if (animateConfig) {
+                var animObj = {
+                    to: {
+                        y: y
+                    },
+                    duration: animateConfig.duration || 125,
+                    listeners: {
+                        afteranimate: function() {
+                            invalidateScroller();
+                            y = row.getXY()[1] - 5;
+                            me.el.setY(y);
+                        }
+                    }
+                };
+                me.animate(animObj);
+            } else {
+                me.el.setY(y);
+                invalidateScroller();
+            }
+        }
+        if (me.getWidth() != mainBodyWidth) {
+            me.setWidth(mainBodyWidth);
         }
+        btnEl.setLeft(left);
     },
 
-    onShow: function(){
-        if(this.monitorValid){
-            this.startMonitoring();
+    getEditor: function(fieldInfo) {
+        var me = this;
+
+        if (Ext.isNumber(fieldInfo)) {
+            // Query only form fields. This just future-proofs us in case we add
+            // other components to RowEditor later on.  Don't want to mess with
+            // indices.
+            return me.query('&gt;[isFormField]')[fieldInfo];
+        } else if (fieldInfo instanceof Ext.grid.column.Column) {
+            return fieldInfo.getEditor();
         }
-        Ext.ux.grid.RowEditor.superclass.onShow.apply(this, arguments);
     },
 
-    onHide: function(){
-        Ext.ux.grid.RowEditor.superclass.onHide.apply(this, arguments);
-        this.stopMonitoring();
-        this.grid.getView().focusRow(this.rowIndex);
+    removeField: function(field) {
+        var me = this;
+
+        // Incase we pass a column instead, which is fine
+        field = me.getEditor(field);
+        me.mun(field, 'validitychange', me.onValidityChange, me);
+
+        // Remove field/column from our mapping, which will fire the event to
+        // remove the field from our container
+        me.columns.removeKey(field.id);
     },
 
-    positionButtons: function(){
-        if(this.btns){
-            var g = this.grid,
-                h = this.el.dom.clientHeight,
-                view = g.getView(),
-                scroll = view.scroller.dom.scrollLeft,
-                bw = this.btns.getWidth(),
-                width = Math.min(g.getWidth(), g.getColumnModel().getTotalWidth());
-                
-            this.btns.el.shift({left: (width/2)-(bw/2)+scroll, top: h - 2, stopFx: true, duration:0.2});
+    setField: function(column) {
+        var me = this,
+            field;
+
+        if (Ext.isArray(column)) {
+            Ext.Array.forEach(column, me.setField, me);
+            return;
         }
-    },
 
-    // private
-    preEditValue : function(r, field){
-        var value = r.data[field];
-        return this.autoEncode && typeof value === 'string' ? Ext.util.Format.htmlDecode(value) : value;
+        // Get a default display field if necessary
+        field = column.getEditor(null, {
+            xtype: 'displayfield',
+            // Default display fields will not return values. This is done because
+            // the display field will pick up column renderers from the grid.
+            getModelData: function() {
+                return null;
+            }
+        });
+        field.margins = '0 0 0 2';
+        field.setWidth(column.getDesiredWidth() - 2);
+        me.mon(field, 'change', me.onFieldChange, me);
+
+        // Maintain mapping of fields-to-columns
+        // This will fire events that maintain our container items
+        me.columns.add(field.id, column);
+        if (column.hidden) {
+            me.onColumnHide(column);
+        }
+        if (me.isVisible() &amp;&amp; me.context) {
+            me.renderColumnData(field, me.context.record);
+        }
     },
 
-    // private
-    postEditValue : function(value, originalValue, r, field){
-        return this.autoEncode && typeof value == 'string' ? Ext.util.Format.htmlEncode(value) : value;
+    loadRecord: function(record) {
+        var me = this,
+            form = me.getForm();
+        form.loadRecord(record);
+        if (form.isValid()) {
+            me.hideToolTip();
+        } else {
+            me.showToolTip();
+        }
+
+        // render display fields so they honor the column renderer/template
+        Ext.Array.forEach(me.query('&gt;displayfield'), function(field) {
+            me.renderColumnData(field, record);
+        }, me);
     },
 
-    doFocus: function(pt){
-        if(this.isVisible()){
-            var index = 0,
-                cm = this.grid.getColumnModel(),
-                c;
-            if(pt){
-                index = this.getTargetColumnIndex(pt);
-            }
-            for(var i = index||0, len = cm.getColumnCount(); i < len; i++){
-                c = cm.getColumnAt(i);
-                if(!c.hidden && c.getEditor()){
-                    c.getEditor().focus();
-                    break;
-                }
-            }
+    renderColumnData: function(field, record) {
+        var me = this,
+            grid = me.editingPlugin.grid,
+            headerCt = grid.headerCt,
+            view = grid.view,
+            store = view.store,
+            column = me.columns.get(field.id),
+            value = record.get(column.dataIndex);
+
+        // honor our column's renderer (TemplateHeader sets renderer for us!)
+        if (column.renderer) {
+            var metaData = { tdCls: '', style: '' },
+                rowIdx = store.indexOf(record),
+                colIdx = headerCt.getHeaderIndex(column);
+
+            value = column.renderer.call(
+                column.scope || headerCt.ownerCt,
+                value,
+                metaData,
+                record,
+                rowIdx,
+                colIdx,
+                store,
+                view
+            );
         }
+
+        field.setRawValue(value);
+        field.resetOriginalValue();
     },
 
-    getTargetColumnIndex: function(pt){
-        var grid = this.grid, 
-            v = grid.view,
-            x = pt.left,
-            cms = grid.colModel.config,
-            i = 0, 
-            match = false;
-        for(var len = cms.length, c; c = cms[i]; i++){
-            if(!c.hidden){
-                if(Ext.fly(v.getHeaderCell(i)).getRegion().right >= x){
-                    match = i;
-                    break;
-                }
-            }
+    beforeEdit: function() {
+        var me = this;
+
+        if (me.isVisible() &amp;&amp; !me.autoCancel &amp;&amp; me.isDirty()) {
+            me.showToolTip();
+            return false;
         }
-        return match;
     },
 
-    startMonitoring : function(){
-        if(!this.bound && this.monitorValid){
-            this.bound = true;
-            Ext.TaskMgr.start({
-                run : this.bindHandler,
-                interval : this.monitorPoll || 200,
-                scope: this
+<span id='Ext-grid-RowEditor-method-startEdit'>    /**
+</span>     * Start editing the specified grid at the specified position.
+     * @param {Ext.data.Model} record The Store data record which backs the row to be edited.
+     * @param {Ext.data.Model} columnHeader The Column object defining the column to be edited.
+     */
+    startEdit: function(record, columnHeader) {
+        var me = this,
+            grid = me.editingPlugin.grid,
+            view = grid.getView(),
+            store = grid.store,
+            context = me.context = Ext.apply(me.editingPlugin.context, {
+                view: grid.getView(),
+                store: store
+            });
+
+        // make sure our row is selected before editing
+        context.grid.getSelectionModel().select(record);
+
+        // Reload the record data
+        me.loadRecord(record);
+
+        if (!me.isVisible()) {
+            me.show();
+            me.focusContextCell();
+        } else {
+            me.reposition({
+                callback: this.focusContextCell
             });
         }
     },
 
-    stopMonitoring : function(){
-        this.bound = false;
-        if(this.tooltip){
-            this.tooltip.hide();
+    // Focus the cell on start edit based upon the current context
+    focusContextCell: function() {
+        var field = this.getEditor(this.context.colIdx);
+        if (field &amp;&amp; field.focus) {
+            field.focus();
         }
     },
 
-    isValid: function(){
-        var valid = true;
-        this.items.each(function(f){
-            if(!f.isValid(true)){
-                valid = false;
-                return false;
-            }
-        });
-        return valid;
+    cancelEdit: function() {
+        var me = this,
+            form = me.getForm();
+
+        me.hide();
+        form.clearInvalid();
+        form.reset();
     },
 
-    // private
-    bindHandler : function(){
-        if(!this.bound){
-            return false; // stops binding
+    completeEdit: function() {
+        var me = this,
+            form = me.getForm();
+
+        if (!form.isValid()) {
+            return;
         }
-        var valid = this.isValid();
-        if(!valid && this.errorSummary){
-            this.showTooltip(this.getErrorText().join(''));
+
+        form.updateRecord(me.context.record);
+        me.hide();
+        return true;
+    },
+
+    onShow: function() {
+        var me = this;
+        me.callParent(arguments);
+        me.reposition();
+    },
+
+    onHide: function() {
+        var me = this;
+        me.callParent(arguments);
+        me.hideToolTip();
+        me.invalidateScroller();
+        if (me.context) {
+            me.context.view.focus();
+            me.context = null;
         }
-        this.btns.saveBtn.setDisabled(!valid);
-        this.fireEvent('validation', this, valid);
-    },
-
-    showTooltip: function(msg){
-        var t = this.tooltip;
-        if(!t){
-            t = this.tooltip = new Ext.ToolTip({
-                maxWidth: 600,
-                cls: 'errorTip',
-                width: 300,
-                title: this.errorText,
+    },
+
+    isDirty: function() {
+        var me = this,
+            form = me.getForm();
+        return form.isDirty();
+    },
+
+    getToolTip: function() {
+        var me = this,
+            tip;
+
+        if (!me.tooltip) {
+            tip = me.tooltip = Ext.createWidget('tooltip', {
+                cls: Ext.baseCSSPrefix + 'grid-row-editor-errors',
+                title: me.errorsText,
                 autoHide: false,
-                anchor: 'left',
-                anchorToTarget: true,
-                mouseOffset: [40,0]
+                closable: true,
+                closeAction: 'disable',
+                anchor: 'left'
             });
         }
-        var v = this.grid.getView(),
-            top = parseInt(this.el.dom.style.top, 10),
-            scroll = v.scroller.dom.scrollTop,
-            h = this.el.getHeight();
-                
-        if(top + h >= scroll){
-            t.initTarget(this.items.last().getEl());
-            if(!t.rendered){
-                t.show();
-                t.hide();
-            }
-            t.body.update(msg);
-            t.doAutoWidth();
-            t.show();
-        }else if(t.rendered){
-            t.hide();
+        return me.tooltip;
+    },
+
+    hideToolTip: function() {
+        var me = this,
+            tip = me.getToolTip();
+        if (tip.rendered) {
+            tip.disable();
         }
+        me.hiddenTip = false;
     },
 
-    getErrorText: function(){
-        var data = ['<ul>'];
-        this.items.each(function(f){
-            if(!f.isValid(true)){
-                data.push('<li>', f.activeError, '</li>');
-            }
-        });
-        data.push('</ul>');
-        return data;
-    }
-});
-Ext.preg('roweditor', Ext.ux.grid.RowEditor);
+    showToolTip: function() {
+        var me = this,
+            tip = me.getToolTip(),
+            context = me.context,
+            row = Ext.get(context.row),
+            viewEl = context.grid.view.el;
+
+        tip.setTarget(row);
+        tip.showAt([-10000, -10000]);
+        tip.body.update(me.getErrors());
+        tip.mouseOffset = [viewEl.getWidth() - row.getWidth() + me.lastScrollLeft + 15, 0];
+        me.repositionTip();
+        tip.doLayout();
+        tip.enable();
+    },
 
-Ext.override(Ext.form.Field, {
-    markInvalid : function(msg){
-        if(!this.rendered || this.preventMark){ // not rendered
-            return;
-        }
-        msg = msg || this.invalidText;
-
-        var mt = this.getMessageHandler();
-        if(mt){
-            mt.mark(this, msg);
-        }else if(this.msgTarget){
-            this.el.addClass(this.invalidClass);
-            var t = Ext.getDom(this.msgTarget);
-            if(t){
-                t.innerHTML = msg;
-                t.style.display = this.msgDisplay;
-            }
+    repositionTip: function() {
+        var me = this,
+            tip = me.getToolTip(),
+            context = me.context,
+            row = Ext.get(context.row),
+            viewEl = context.grid.view.el,
+            viewHeight = viewEl.getHeight(),
+            viewTop = me.lastScrollTop,
+            viewBottom = viewTop + viewHeight,
+            rowHeight = row.getHeight(),
+            rowTop = row.dom.offsetTop,
+            rowBottom = rowTop + rowHeight;
+
+        if (rowBottom &gt; viewTop &amp;&amp; rowTop &lt; viewBottom) {
+            tip.show();
+            me.hiddenTip = false;
+        } else {
+            tip.hide();
+            me.hiddenTip = true;
         }
-        this.activeError = msg;
-        this.fireEvent('invalid', this, msg);
-    }
-});
+    },
 
-Ext.override(Ext.ToolTip, {
-    doAutoWidth : function(){
-        var bw = this.body.getTextWidth();
-        if(this.title){
-            bw = Math.max(bw, this.header.child('span').getTextWidth(this.title));
-        }
-        bw += this.getFrameWidth() + (this.closable ? 20 : 0) + this.body.getPadding("lr") + 20;
-        this.setWidth(bw.constrain(this.minWidth, this.maxWidth));
+    getErrors: function() {
+        var me = this,
+            dirtyText = !me.autoCancel &amp;&amp; me.isDirty() ? me.dirtyText + '&lt;br /&gt;' : '',
+            errors = [];
+
+        Ext.Array.forEach(me.query('&gt;[isFormField]'), function(field) {
+            errors = errors.concat(
+                Ext.Array.map(field.getErrors(), function(e) {
+                    return '&lt;li&gt;' + e + '&lt;/li&gt;';
+                })
+            );
+        }, me);
+
+        return dirtyText + '&lt;ul&gt;' + errors.join('') + '&lt;/ul&gt;';
+    },
+
+    invalidateScroller: function() {
+        var me = this,
+            context = me.context,
+            scroller = context.grid.verticalScroller;
 
-        // IE7 repaint bug on initial show
-        if(Ext.isIE7 && !this.repainted){
-            this.el.repaint();
-            this.repainted = true;
+        if (scroller) {
+            scroller.invalidate();
         }
     }
-});
-</pre>
+});</pre>
 </body>
-</html>
\ No newline at end of file
+</html>