    // 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)

    /**
     * @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.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,

        // 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;
            }

            form = me.getForm();
            form.trackResetOnLoad = true;
        },

        onFieldChange: function() {
            var me = this,
                form = me.getForm(),
                valid = form.isValid();
            if (me.errorSummary && me.isVisible()) {
                me[valid ? 'hideToolTip' : 'showToolTip']();
            }
            if (me.floatingButtons) {
                me.floatingButtons.child('#update').setDisabled(!valid);
            }
            me.isValid = valid;
        },

        afterRender: function() {
            var me = this,
                plugin = me.editingPlugin;

            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
            });
        },

        onBeforeViewRefresh: function(view) {
            var me = this,
                viewDom = view.el.dom;

            if (me.el.dom.parentNode === viewDom) {
                viewDom.removeChild(me.el.dom);
            }
        },

        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 && (idx = context.store.indexOf(context.record)) >= 0) {
                context.row = view.getNode(idx);
                me.reposition();
                if (me.tooltip && me.tooltip.isVisible()) {
                    me.tooltip.setTarget(context.row);
                }
            } else {
                me.editingPlugin.cancelEdit();
            }
        },

        onCtScroll: function(e, target) {
            var me = this,
                scrollTop  = target.scrollTop,
                scrollLeft = target.scrollLeft;

            if (scrollTop !== me.lastScrollTop) {
                me.lastScrollTop = scrollTop;
                if ((me.tooltip && me.tooltip.isVisible()) || me.hiddenTip) {
                    me.repositionTip();
                }
            }
            if (scrollLeft !== me.lastScrollLeft) {
                me.lastScrollLeft = scrollLeft;
                me.reposition();
            }
        },

        onColumnAdd: function(column) {
            this.setField(column);
        },

        onColumnRemove: function(column) {
            this.columns.remove(column);
        },

        onColumnResize: function(column, width) {
            column.getEditor().setWidth(width - 2);
            if (this.isVisible()) {
                this.reposition();
            }
        },

        onColumnHide: function(column) {
            column.getEditor().hide();
            if (this.isVisible()) {
                this.reposition();
            }
        },

        onColumnShow: function(column) {
            var field = column.getEditor();
            field.setWidth(column.getWidth() - 2).show();
            if (this.isVisible()) {
                this.reposition();
            }
        },

        onColumnMove: function(column, fromIdx, toIdx) {
            var field = column.getEditor();
            if (this.items.indexOf(field) != toIdx) {
                this.move(fromIdx, toIdx);
            }
        },

        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();
            }
        },

        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);
            });
        },

        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: [
                        '<div class="{baseCls}-ml"></div>',
                        '<div class="{baseCls}-mr"></div>',
                        '<div class="{baseCls}-bl"></div>',
                        '<div class="{baseCls}-br"></div>',
                        '<div class="{baseCls}-bc"></div>'
                    ],

                    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 && 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 && animateConfig.callback) {
                        animateConfig.callback.call(animateConfig.scope || me);
                    }
                };

            // need to set both top/left
            if (row && 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);
        },

        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('>[isFormField]')[fieldInfo];
            } else if (fieldInfo instanceof Ext.grid.column.Column) {
                return fieldInfo.getEditor();
            }
        },

        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);
        },

        setField: function(column) {
            var me = this,
                field;

            if (Ext.isArray(column)) {
                Ext.Array.forEach(column, me.setField, me);
                return;
            }

            // 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 (me.isVisible() && me.context) {
                me.renderColumnData(field, me.context.record);
            }
        },

        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('>displayfield'), function(field) {
                me.renderColumnData(field, record);
            }, me);
        },

        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();
        },

        beforeEdit: function() {
            var me = this;

            if (me.isVisible() && !me.autoCancel && me.isDirty()) {
                me.showToolTip();
                return false;
            }
        },

        /**
         * Start editing the specified grid at the specified position.
         * @param {Model} record The Store data record which backs the row to be edited.
         * @param {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
                });
            }
        },

        // Focus the cell on start edit based upon the current context
        focusContextCell: function() {
            var field = this.getEditor(this.context.colIdx);
            if (field && field.focus) {
                field.focus();
            }
        },

        cancelEdit: function() {
            var me = this,
                form = me.getForm();

            me.hide();
            form.clearInvalid();
            form.reset();
        },

        completeEdit: function() {
            var me = this,
                form = me.getForm();

            if (!form.isValid()) {
                return;
            }

            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;
            }
        },

        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,
                    closable: true,
                    closeAction: 'disable',
                    anchor: 'left'
                });
            }
            return me.tooltip;
        },

        hideToolTip: function() {
            var me = this,
                tip = me.getToolTip();
            if (tip.rendered) {
                tip.disable();
            }
            me.hiddenTip = false;
        },

        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();
        },

        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 > viewTop && rowTop < viewBottom) {
                tip.show();
                me.hiddenTip = false;
            } else {
                tip.hide();
                me.hiddenTip = true;
            }
        },

        getErrors: function() {
            var me = this,
                dirtyText = !me.autoCancel && me.isDirty() ? me.dirtyText + '<br />' : '',
                errors = [];

            Ext.Array.forEach(me.query('>[isFormField]'), function(field) {
                errors = errors.concat(
                    Ext.Array.map(field.getErrors(), function(e) {
                        return '<li>' + e + '</li>';
                    })
                );
            }, me);

            return dirtyText + '<ul>' + errors.join('') + '</ul>';
        },

        invalidateScroller: function() {
            var me = this,
                context = me.context,
                scroller = context.grid.verticalScroller;

            if (scroller) {
                scroller.invalidate();
            }
        }
    });