
/**
 * @class Ext.grid.plugin.Editing

This class provides an abstract grid editing plugin on selected {@link Ext.grid.column.Column columns}.
The editable columns are specified by providing an {@link Ext.grid.column.Column#editor editor}
in the {@link Ext.grid.column.Column column configuration}.

*Note:* This class should not be used directly. See {@link Ext.grid.plugin.CellEditing} and
{@link Ext.grid.plugin.RowEditing}.

 * @markdown
 */
Ext.define('Ext.grid.plugin.Editing', {
    alias: 'editing.editing',

    requires: [
        'Ext.grid.column.Column',
        'Ext.util.KeyNav'
    ],

    mixins: {
        observable: 'Ext.util.Observable'
    },

    /**
     * @cfg {Number} clicksToEdit
     * The number of clicks on a grid required to display the editor (defaults to 2).
     */
    clicksToEdit: 2,

    // private
    defaultFieldXType: 'textfield',

    // cell, row, form
    editStyle: '',

    constructor: function(config) {
        var me = this;
        Ext.apply(me, config);

        me.addEvents(
            // Doc'ed in separate editing plugins
            'beforeedit',

            // Doc'ed in separate editing plugins
            'edit',

            // Doc'ed in separate editing plugins
            'validateedit'
        );
        me.mixins.observable.constructor.call(me);
        // TODO: Deprecated, remove in 5.0
        me.relayEvents(me, ['afteredit'], 'after');
    },

    // private
    init: function(grid) {
        var me = this;

        me.grid = grid;
        me.view = grid.view;
        me.initEvents();
        me.initFieldAccessors(me.view.getGridColumns());

        grid.relayEvents(me, ['beforeedit', 'edit', 'validateedit']);
        // Marks the grid as editable, so that the SelectionModel
        // can make appropriate decisions during navigation
        grid.isEditable = true;
        grid.editingPlugin = grid.view.editingPlugin = me;
    },

    /**
     * @private
     * AbstractComponent calls destroy on all its plugins at destroy time.
     */
    destroy: function() {
        var me = this,
            grid = me.grid,
            headerCt = grid.headerCt,
            events = grid.events;

        Ext.destroy(me.keyNav);
        me.removeFieldAccessors(grid.getView().getGridColumns());

        // Clear all listeners from all our events, clear all managed listeners we added to other Observables
        me.clearListeners();

        delete me.grid.editingPlugin;
        delete me.grid.view.editingPlugin;
        delete me.grid;
        delete me.view;
        delete me.editor;
        delete me.keyNav;
    },

    // private
    getEditStyle: function() {
        return this.editStyle;
    },

    // private
    initFieldAccessors: function(column) {
        var me = this;

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

        // Augment the Header class to have a getEditor and setEditor method
        // Important: Only if the header does not have its own implementation.
        Ext.applyIf(column, {
            getEditor: function(record, defaultField) {
                return me.getColumnField(this, defaultField);
            },

            setEditor: function(field) {
                me.setColumnField(this, field);
            }
        });
    },

    // private
    removeFieldAccessors: function(column) {
        var me = this;

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

        delete column.getEditor;
        delete column.setEditor;
    },

    // private
    // remaps to the public API of Ext.grid.column.Column.getEditor
    getColumnField: function(columnHeader, defaultField) {
        var field = columnHeader.field;

        if (!field && columnHeader.editor) {
            field = columnHeader.editor;
            delete columnHeader.editor;
        }

        if (!field && defaultField) {
            field = defaultField;
        }

        if (field) {
            if (Ext.isString(field)) {
                field = { xtype: field };
            }
            if (Ext.isObject(field) && !field.isFormField) {
                field = Ext.ComponentManager.create(field, this.defaultFieldXType);
                columnHeader.field = field;
            }

            Ext.apply(field, {
                name: columnHeader.dataIndex
            });

            return field;
        }
    },

    // private
    // remaps to the public API of Ext.grid.column.Column.setEditor
    setColumnField: function(column, field) {
        if (Ext.isObject(field) && !field.isFormField) {
            field = Ext.ComponentManager.create(field, this.defaultFieldXType);
        }
        column.field = field;
    },

    // private
    initEvents: function() {
        var me = this;
        me.initEditTriggers();
        me.initCancelTriggers();
    },

    // @abstract
    initCancelTriggers: Ext.emptyFn,

    // private
    initEditTriggers: function() {
        var me = this,
            view = me.view,
            clickEvent = me.clicksToEdit === 1 ? 'click' : 'dblclick';

        // Start editing
        me.mon(view, 'cell' + clickEvent, me.startEditByClick, me);
        view.on('render', function() {
            me.keyNav = Ext.create('Ext.util.KeyNav', view.el, {
                enter: me.onEnterKey,
                esc: me.onEscKey,
                scope: me
            });
        }, me, { single: true });
    },

    // private
    onEnterKey: function(e) {
        var me = this,
            grid = me.grid,
            selModel = grid.getSelectionModel(),
            record,
            columnHeader = grid.headerCt.getHeaderAtIndex(0);

        // Calculate editing start position from SelectionModel
        // CellSelectionModel
        if (selModel.getCurrentPosition) {
            pos = selModel.getCurrentPosition();
            record = grid.store.getAt(pos.row);
            columnHeader = grid.headerCt.getHeaderAtIndex(pos.column);
        }
        // RowSelectionModel
        else {
            record = selModel.getLastSelected();
        }
        me.startEdit(record, columnHeader);
    },

    // private
    onEscKey: function(e) {
        this.cancelEdit();
    },

    // private
    startEditByClick: function(view, cell, colIdx, record, row, rowIdx, e) {
        this.startEdit(record, view.getHeaderAtIndex(colIdx));
    },

    /**
     * @private
     * @abstract. Template method called before editing begins.
     * @param {Object} context The current editing context
     * @return {Boolean} Return false to cancel the editing process
     */
    beforeEdit: Ext.emptyFn,

    /**
     * Start editing the specified record, using the specified Column definition to define which field is being edited.
     * @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,
            context = me.getEditingContext(record, columnHeader);

        if (me.beforeEdit(context) === false || me.fireEvent('beforeedit', context) === false || context.cancel) {
            return false;
        }

        me.context = context;
        me.editing = true;
    },

    /**
     * @private Collects all information necessary for any subclasses to perform their editing functions.
     * @param record
     * @param columnHeader
     * @returns {Object} The editing context based upon the passed record and column
     */
    getEditingContext: function(record, columnHeader) {
        var me = this,
            grid = me.grid,
            store = grid.store,
            rowIdx,
            colIdx,
            view = grid.getView(),
            value;

        // If they'd passed numeric row, column indices, look them up.
        if (Ext.isNumber(record)) {
            rowIdx = record;
            record = store.getAt(rowIdx);
        } else {
            rowIdx = store.indexOf(record);
        }
        if (Ext.isNumber(columnHeader)) {
            colIdx = columnHeader;
            columnHeader = grid.headerCt.getHeaderAtIndex(colIdx);
        } else {
            colIdx = columnHeader.getIndex();
        }

        value = record.get(columnHeader.dataIndex);
        return {
            grid: grid,
            record: record,
            field: columnHeader.dataIndex,
            value: value,
            row: view.getNode(rowIdx),
            column: columnHeader,
            rowIdx: rowIdx,
            colIdx: colIdx
        };
    },

    /**
     * Cancel any active edit that is in progress.
     */
    cancelEdit: function() {
        this.editing = false;
    },

    /**
     * Complete the edit if there is an active edit in progress.
     */
    completeEdit: function() {
        var me = this;

        if (me.editing && me.validateEdit()) {
            me.fireEvent('edit', me.context);
        }

        delete me.context;
        me.editing = false;
    },

    // @abstract
    validateEdit: function() {
        var me = this,
            context = me.context;

        return me.fireEvent('validateedit', me, context) !== false && !context.cancel;
    }
});