/** * 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}. */ 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. */ clicksToEdit: 2, /** * @cfg {String} triggerEvent * The event which triggers editing. Supercedes the {@link clicksToEdit} configuration. Maybe one of: * * * cellclick * * celldblclick * * cellfocus * * rowfocus */ triggerEvent: undefined, // private defaultFieldXType: 'textfield', // cell, row, form editStyle: '', constructor: function(config) { var me = this; Ext.apply(me, config); me.addEvents( /** * @event beforeedit * Fires before editing is triggered. Return false from event handler to stop the editing. * * @param {Ext.grid.plugin.Editing} editor * @param {Object} e An edit event with the following properties: * * - grid - The grid * - record - The record being edited * - field - The field name being edited * - value - The value for the field being edited. * - row - The grid table row * - column - The grid {@link Ext.grid.column.Column Column} defining the column that is being edited. * - rowIdx - The row index that is being edited * - colIdx - The column index that is being edited * - cancel - Set this to true to cancel the edit or return false from your handler. * - originalValue - Alias for value (only when using {@link Ext.grid.plugin.CellEditing CellEditing}). */ 'beforeedit', /** * @event edit * Fires after a editing. Usage example: * * grid.on('edit', function(editor, e) { * // commit the changes right after editing finished * e.record.commit(); * }); * * @param {Ext.grid.plugin.Editing} editor * @param {Object} e An edit event with the following properties: * * - grid - The grid * - record - The record that was edited * - field - The field name that was edited * - value - The value being set * - row - The grid table row * - column - The grid {@link Ext.grid.column.Column Column} defining the column that was edited. * - rowIdx - The row index that was edited * - colIdx - The column index that was edited * - originalValue - The original value for the field, before the edit (only when using {@link Ext.grid.plugin.CellEditing CellEditing}) * - originalValues - The original values for the field, before the edit (only when using {@link Ext.grid.plugin.RowEditing RowEditing}) * - newValues - The new values being set (only when using {@link Ext.grid.plugin.RowEditing RowEditing}) * - view - The grid view (only when using {@link Ext.grid.plugin.RowEditing RowEditing}) * - store - The grid store (only when using {@link Ext.grid.plugin.RowEditing RowEditing}) */ 'edit', /** * @event validateedit * Fires after editing, but before the value is set in the record. Return false from event handler to * cancel the change. * * Usage example showing how to remove the red triangle (dirty record indicator) from some records (not all). By * observing the grid's validateedit event, it can be cancelled if the edit occurs on a targeted row (for example) * and then setting the field's new value in the Record directly: * * grid.on('validateedit', function(editor, e) { * var myTargetRow = 6; * * if (e.rowIdx == myTargetRow) { * e.cancel = true; * e.record.data[e.field] = e.value; * } * }); * * @param {Ext.grid.plugin.Editing} editor * @param {Object} e An edit event with the following properties: * * - grid - The grid * - record - The record being edited * - field - The field name being edited * - value - The value being set * - row - The grid table row * - column - The grid {@link Ext.grid.column.Column Column} defining the column that is being edited. * - rowIdx - The row index that is being edited * - colIdx - The column index that is being edited * - cancel - Set this to true to cancel the edit or return false from your handler. * - originalValue - The original value for the field, before the edit (only when using {@link Ext.grid.plugin.CellEditing CellEditing}) * - originalValues - The original values for the field, before the edit (only when using {@link Ext.grid.plugin.RowEditing RowEditing}) * - newValues - The new values being set (only when using {@link Ext.grid.plugin.RowEditing RowEditing}) * - view - The grid view (only when using {@link Ext.grid.plugin.RowEditing RowEditing}) * - store - The grid store (only when using {@link Ext.grid.plugin.RowEditing RowEditing}) */ 'validateedit', /** * @event canceledit * Fires when the user started editing but then cancelled the edit. * @param {Ext.grid.plugin.Editing} editor * @param {Object} e An edit event with the following properties: * * - grid - The grid * - record - The record that was edited * - field - The field name that was edited * - value - The value being set * - row - The grid table row * - column - The grid {@link Ext.grid.column.Column Column} defining the column that was edited. * - rowIdx - The row index that was edited * - colIdx - The column index that was edited * - view - The grid view * - store - The grid store */ 'canceledit' ); me.mixins.observable.constructor.call(me); // TODO: Deprecated, remove in 5.0 me.on("edit", function(editor, e) { me.fireEvent("afteredit", editor, e); }); }, // private init: function(grid) { var me = this; // If the plugin owner is a lockable grid, attach to its normal (right side) grid. if (grid.lockable) { grid = grid.view.normalGrid; } me.grid = grid; me.view = grid.view; me.initEvents(); me.mon(grid, 'reconfigure', me.onReconfigure, me); me.onReconfigure(); grid.relayEvents(me, [ /** * @event beforeedit * Forwarded event from Ext.grid.plugin.Editing. * @member Ext.panel.Table * @inheritdoc Ext.grid.plugin.Editing#beforeedit */ 'beforeedit', /** * @event edit * Forwarded event from Ext.grid.plugin.Editing. * @member Ext.panel.Table * @inheritdoc Ext.grid.plugin.Editing#edit */ 'edit', /** * @event validateedit * Forwarded event from Ext.grid.plugin.Editing. * @member Ext.panel.Table * @inheritdoc Ext.grid.plugin.Editing#validateedit */ 'validateedit', /** * @event canceledit * Forwarded event from Ext.grid.plugin.Editing. * @member Ext.panel.Table * @inheritdoc Ext.grid.plugin.Editing#canceledit */ 'canceledit' ]); // Marks the grid as editable, so that the SelectionModel // can make appropriate decisions during navigation grid.isEditable = true; grid.editingPlugin = grid.view.editingPlugin = me; }, /** * Fires after the grid is reconfigured * @private */ onReconfigure: function(){ this.initFieldAccessors(this.view.getGridColumns()); }, /** * @private * AbstractComponent calls destroy on all its plugins at destroy time. */ destroy: function() { var me = this, grid = me.grid; 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(columns) { columns = [].concat(columns); var me = this, c, cLen = columns.length, column; for (c = 0; c < cLen; c++) { column = columns[c]; Ext.applyIf(column, { getEditor: function(record, defaultField) { return me.getColumnField(this, defaultField); }, setEditor: function(field) { me.setColumnField(this, field); } }); } }, // private removeFieldAccessors: function(columns) { columns = [].concat(columns); var c, cLen = columns.length, column; for (c = 0; c < cLen; c++) { column = columns[c]; 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 (!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; // Listen for the edit trigger event. if (me.triggerEvent == 'cellfocus') { me.mon(view, 'cellfocus', me.onCellFocus, me); } else if (me.triggerEvent == 'rowfocus') { me.mon(view, 'rowfocus', me.onRowFocus, me); } else { // Prevent the View from processing when the SelectionModel focuses. // This is because the SelectionModel processes the mousedown event, and // focusing causes a scroll which means that the subsequent mouseup might // take place at a different document XY position, and will therefore // not trigger a click. // This Editor must call the View's focusCell method directly when we recieve a request to edit if (view.selModel.isCellModel) { view.onCellFocus = Ext.Function.bind(me.beforeViewCellFocus, me); } // Listen for whichever click event we are configured to use me.mon(view, me.triggerEvent || ('cell' + (me.clicksToEdit === 1 ? 'click' : 'dblclick')), me.onCellClick, me); } view.on('render', me.addHeaderEvents, me, {single: true}); }, // Override of View's method so that we can pre-empt the View's processing if the view is being triggered by a mousedown beforeViewCellFocus: function(position) { // Pass call on to view if the navigation is from the keyboard, or we are not going to edit this cell. if (this.view.selModel.keyNavigation || !this.isCellEditable || !this.isCellEditable(position.row, position.columnHeader)) { this.view.focusCell.apply(this.view, arguments); } }, // private. Used if we are triggered by the rowfocus event onRowFocus: function(record, row, rowIdx) { this.startEdit(row, 0); }, // private. Used if we are triggered by the cellfocus event onCellFocus: function(record, cell, position) { this.startEdit(position.row, position.column); }, // private. Used if we are triggered by a cellclick event onCellClick: function(view, cell, colIdx, record, row, rowIdx, e) { // cancel editing if the element that was clicked was a tree expander if(!view.expanderSelector || !e.getTarget(view.expanderSelector)) { this.startEdit(record, view.getHeaderAtIndex(colIdx)); } }, addHeaderEvents: function(){ var me = this; me.mon(me.grid.headerCt, { scope: me, add: me.onColumnAdd, remove: me.onColumnRemove }); me.keyNav = Ext.create('Ext.util.KeyNav', me.view.el, { enter: me.onEnterKey, esc: me.onEscKey, scope: me }); }, // private onColumnAdd: function(ct, column) { if (column.isHeader) { this.initFieldAccessors(column); } }, // private onColumnRemove: function(ct, column) { if (column.isHeader) { this.removeFieldAccessors(column); } }, // private onEnterKey: function(e) { var me = this, grid = me.grid, selModel = grid.getSelectionModel(), record, pos, columnHeader = grid.headerCt.getHeaderAtIndex(0); // Calculate editing start position from SelectionModel // CellSelectionModel if (selModel.getCurrentPosition) { pos = selModel.getCurrentPosition(); if (pos) { record = grid.store.getAt(pos.row); columnHeader = grid.headerCt.getHeaderAtIndex(pos.column); } } // RowSelectionModel else { record = selModel.getLastSelected(); } // If there was a selection to provide a starting context... if (record && columnHeader) { me.startEdit(record, columnHeader); } }, // private onEscKey: function(e) { this.cancelEdit(); }, /** * @private * @template * 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, /** * Starts editing the specified record, using the specified Column definition to define which field is being edited. * @param {Ext.data.Model/Number} record The Store data record which backs the row to be edited, or index of the record in Store. * @param {Ext.grid.column.Column/Number} columnHeader The Column object defining the column to be edited, or index of the column. */ startEdit: function(record, columnHeader) { var me = this, context = me.getEditingContext(record, columnHeader); if (me.beforeEdit(context) === false || me.fireEvent('beforeedit', me, context) === false || context.cancel || !me.grid.view.isVisible(true)) { return false; } me.context = context; me.editing = true; }, // TODO: Have this use a new class Ext.grid.CellContext for use here, and in CellSelectionModel /** * @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, view = grid.getView(), node = view.getNode(record), rowIdx, colIdx; // An intervening listener may have deleted the Record if (!node) { return; } // Coerce the column index to the closest visible column columnHeader = grid.headerCt.getVisibleHeaderClosestToIndex(Ext.isNumber(columnHeader) ? columnHeader : columnHeader.getIndex()); // No corresponding column. Possible if all columns have been moved to the other side of a lockable grid pair if (!columnHeader) { return; } colIdx = columnHeader.getIndex(); if (Ext.isNumber(record)) { // look up record if numeric row index was passed rowIdx = record; record = view.getRecord(node); } else { rowIdx = view.indexOf(node); } return { grid : grid, record : record, field : columnHeader.dataIndex, value : record.get(columnHeader.dataIndex), row : view.getNode(rowIdx), column : columnHeader, rowIdx : rowIdx, colIdx : colIdx }; }, /** * Cancels any active edit that is in progress. */ cancelEdit: function() { var me = this; me.editing = false; me.fireEvent('canceledit', me, me.context); }, /** * Completes the edit if there is an active edit in progress. */ completeEdit: function() { var me = this; if (me.editing && me.validateEdit()) { me.fireEvent('edit', me, 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; } });