/** * The Ext.grid.plugin.RowEditing plugin injects editing at a row level for a Grid. When editing * begins, a small floating dialog will be shown for the appropriate row. Each editable column * will show a field for editing. There is a button to save or cancel all changes for the edit. * * The field that will be used for the editor is defined at the * {@link Ext.grid.column.Column#editor editor}. The editor can be a field instance or a field * configuration. If an editor is not specified for a particular column then that column * won't be editable and the value of the column will be displayed. To provide a custom renderer * for non-editable values, use the * {@link Ext.grid.column.Column#editRenderer editRenderer} configuration on the column. * * The editor may be shared for each column in the grid, or a different one may be specified * for each column. An appropriate field type should be chosen to match the data structure * that it will be editing. For example, to edit a date, it would be useful to specify * {@link Ext.form.field.Date} as the editor. * * @example * Ext.create('Ext.data.Store', { * storeId: 'simpsonsStore', * fields:[ 'name', 'email', 'phone'], * data: [ * { name: 'Lisa', email: '[email protected]', phone: '555-111-1224' }, * { name: 'Bart', email: '[email protected]', phone: '555-222-1234' }, * { name: 'Homer', email: '[email protected]', phone: '555-222-1244' }, * { name: 'Marge', email: '[email protected]', phone: '555-222-1254' } * ] * }); * * Ext.create('Ext.grid.Panel', { * title: 'Simpsons', * store: Ext.data.StoreManager.lookup('simpsonsStore'), * columns: [ * {header: 'Name', dataIndex: 'name', editor: 'textfield'}, * {header: 'Email', dataIndex: 'email', flex:1, * editor: { * xtype: 'textfield', * allowBlank: false * } * }, * {header: 'Phone', dataIndex: 'phone'} * ], * selModel: 'rowmodel', * plugins: { * rowediting: { * clicksToEdit: 1 * } * }, * height: 200, * width: 400, * renderTo: Ext.getBody() * }); * */Ext.define('Ext.grid.plugin.RowEditing', { extend: 'Ext.grid.plugin.Editing', alias: 'plugin.rowediting', requires: [ 'Ext.grid.RowEditor' ], lockableScope: 'top', editStyle: 'row', /** * @cfg {Boolean} [autoCancel=true] * `true` to automatically cancel any pending changes when the row editor begins editing * a new row. `false` to force the user to explicitly cancel the pending changes. * Note that this option is mutually exclusive with {@link #autoUpdate}. */ autoCancel: true, /** * @cfg {Boolean} [autoUpdate=false] * Set this to `true` to automatically confirm any pending changes when the row editor * begins editing a new row. When `false`, the user will need to explicitly confirm * the pending changes. * Note that if this is set to `true`, {@link #autoCancel} will be set to `false`. */ autoUpdate: false, /** * @cfg {Boolean} [removeUnmodified=false] * If configured as `true`, then canceling an edit on a newly inserted * record which has not been modified will delete that record from the store. */ /** * @cfg {Number} clicksToMoveEditor * The number of clicks to move the row editor to a new row while it is visible and actively * editing another row. This will default to the same value as * {@link Ext.grid.plugin.Editing#clicksToEdit clicksToEdit}. */ /** * @cfg {Boolean} errorSummary * True to show a {@link Ext.tip.ToolTip tooltip} that summarizes all validation errors present * in the row editor. Set to false to prevent the tooltip from showing. */ errorSummary: true, /** * @cfg {String} [formAriaLabel="'Editing row {0}'"] * The ARIA label template for screen readers to announce when row editing starts. * This label can be a {@link Ext.String#format} template, with the only parameter * being the row number. Note that row numbers start at base {@link #formAriaLabelRowBase}. * @locale */ formAriaLabel: 'Editing row {0}', /** * @cfg {Number} [formAriaLabelRowBase=2] * Screen readers will announce grid column header as first row of the ARIA table, * so the first actual data row is #2 for screen reader users. If your grid has * more than one column header row, you might want to increase this number. * If the column header is not visible, the base will be decreased automatically. * @locale */ formAriaLabelRowBase: 2, constructor: function() { var me = this; me.callParent(arguments); if (!me.clicksToMoveEditor) { me.clicksToMoveEditor = me.clicksToEdit; } me.autoCancel = !!me.autoCancel; me.autoUpdate = !!me.autoUpdate; if (me.autoUpdate) { me.autoCancel = false; } }, init: function(grid) { this.callParent([grid]); // This plugin has an interest in processing a request for actionable mode. // It does not actually enter actionable mode, it just calls startEdit if (grid.lockedGrid) { grid.lockedGrid.registerActionable(this); grid.normalGrid.registerActionable(this); } else { grid.registerActionable(this); } }, destroy: function() { Ext.destroy(this.editor); this.callParent(); }, onBeforeReconfigure: function() { this.callParent(arguments); this.cancelEdit(); }, onReconfigure: function(grid, store, columns) { var ed = this.editor; this.callParent(arguments); // Only need to adjust column widths if we have new columns if (columns && ed && ed.rendered) { ed.needsSyncFieldWidths = true; } }, shouldStartEdit: function(editor) { return true; }, /** * Starts editing the specified record, using the specified Column definition to define * which field is being edited. * @param {Ext.data.Model} record The Store data record which backs the row to be edited. * @param {Ext.grid.column.Column/Number} [columnHeader] The Column object defining * the column field to be focused, or index of the column. If not specified, it will default * to the first visible column. * @return {Boolean} `true` if editing was started, `false` otherwise. */ startEdit: function(record, columnHeader) { var me = this, editor = me.getEditor(), context; if (Ext.isEmpty(columnHeader)) { columnHeader = me.grid.getTopLevelVisibleColumnManager().getHeaderAtIndex(0); } if (editor.beforeEdit() !== false) { context = me.getEditingContext(record, columnHeader, true); if (context && me.beforeEdit(context) !== false && me.fireEvent('beforeedit', me, context) !== false && !context.cancel) { me.context = context; // If editing one side of a lockable grid, cancel any edit on the other side. if (me.lockingPartner) { me.lockingPartner.cancelEdit(); } editor.startEdit(context.record, context.column, context); me.editing = true; return true; } } return false; }, /** * This method is called when actionable mode is requested for a cell. * @param {Ext.grid.CellContext} pos The position at which actionable mode was requested. * @return {Boolean} `false` Actionable mode is *not* entered for RowEditing. * @protected */ activateCell: function(pos) { // Only activate editing if there are no readily activatable elements // in the activate position. // We defer to those focusables. Editing may be started on other columns. if (!pos.getCell(true).querySelector('[tabIndex="-1"]')) { this.startEdit(pos.record, pos.column); return true; } }, /** * @method * Called by TableView#suspendActionableMode to suspend actionable processing while * the view is being changed. * @protected */ suspend: Ext.emptyFn, /** * @method * Called by TableView#resumeActionableMode to resume actionable processing after * the view has been changed. * @param {Ext.grid.CellContext} position The position at which to resume actionable processing. * @return {Boolean} `true` if this Actionable has successfully resumed. * @protected */ resume: Ext.emptyFn, /** * @private * The {@link Ext.grid.RowEditor RowEditor} hooks up a KeyNav to call this method * to complete the edit. */ onEnterKey: function(e) { var me = this, targetComponent; // KeyMap entry for EnterKey added after the entry that sets actionable mode, // so this will get called after that handler. We must ignore ENTER key in actionable mode. if (!me.grid.ownerGrid.actionableMode && me.editing) { targetComponent = Ext.getCmp(e.getTarget().getAttribute('componentId')); // ENTER when a picker is expanded does not complete the edit if (!(targetComponent && targetComponent.isPickerField && targetComponent.isExpanded)) { me.completeEdit(); } } }, cancelEdit: function() { var me = this; if (me.editing) { me.getContextFieldValues(); me.getEditor().cancelEdit(); me.callParent(arguments); return; } // If we aren't editing, return true to allow the event to bubble return true; }, completeEdit: function() { var me = this, context = me.context; if (me.editing && me.validateEdit(context)) { me.editing = false; me.fireEvent('edit', me, context); } }, validateEdit: function() { this.getContextFieldValues(); return this.callParent(arguments) && this.getEditor().completeEdit(); }, getEditor: function() { var me = this; if (!me.editor) { me.editor = me.initEditor(); } return me.editor; }, getContextFieldValues: function() { var editor = this.editor, context = this.context, record = context.record, newValues = {}, originalValues = {}, editors = editor.query('>[isFormField]'), len = editors.length, i, name, item; for (i = 0; i < len; i++) { item = editors[i]; name = item.dataIndex; newValues[name] = item.getValue(); originalValues[name] = record.get(name); } Ext.apply(context, { newValues: newValues, originalValues: originalValues }); }, /** * @private */ initEditor: function() { return new Ext.grid.RowEditor(this.initEditorConfig()); }, initEditorConfig: function() { var me = this, grid = me.grid, view = me.view, headerCt = grid.headerCt, btns = ['saveBtnText', 'cancelBtnText', 'errorsText', 'dirtyText'], b, bLen = btns.length, cfg = { autoCancel: me.autoCancel, autoUpdate: me.autoUpdate, removeUnmodified: me.removeUnmodified, errorSummary: me.errorSummary, formAriaLabel: me.formAriaLabel, formAriaLabelRowBase: me.formAriaLabelRowBase + (grid.hideHeaders ? -1 : 0), fields: headerCt.getGridColumns(), hidden: true, view: view, // keep a reference.. editingPlugin: me }, item; for (b = 0; b < bLen; b++) { item = btns[b]; if (Ext.isDefined(me[item])) { cfg[item] = me[item]; } } return cfg; }, /** * @private */ initEditTriggers: function() { var me = this, view = me.view, moveEditorEvent = me.clicksToMoveEditor === 1 ? 'click' : 'dblclick'; me.callParent(arguments); if (me.clicksToMoveEditor !== me.clicksToEdit) { me.mon(view, 'cell' + moveEditorEvent, me.moveEditorByClick, me); } view.on({ render: function() { me.mon(me.grid.headerCt, { scope: me, columnresize: me.onColumnResize, columnhide: me.onColumnHide, columnshow: me.onColumnShow }); }, single: true }); }, moveEditorByClick: function() { var me = this; if (me.editing) { me.superclass.onCellClick.apply(me, arguments); } }, /** * @private */ onColumnAdd: function(ct, column, pos) { var me = this, editor; if (column.isHeader) { me.initFieldAccessors(column); // Only inform the editor about a new column if the editor has already been // instantiated, so do not use getEditor which instantiates the editor if not present. editor = me.editor; if (editor) { editor.onColumnAdd(column, pos); } } }, // Ensure editors are cleaned up. beforeGridHeaderDestroy: function(headerCt) { var columns = this.grid.getColumnManager().getColumns(), len = columns.length, i, column, field; for (i = 0; i < len; i++) { column = columns[i]; // If it has a field accessor, then destroy any field, and remove the accessors. if (column.hasEditor) { if (column.hasEditor() && (field = column.getEditor())) { field.destroy(); } this.removeFieldAccessors(column); } } }, /** * @private */ onColumnResize: function(ct, column, width) { var me = this, editor; if (column.isHeader) { editor = me.getEditor(); if (editor) { editor.onColumnResize(column, width); } } }, /** * @private */ onColumnHide: function(ct, column) { // no isHeader check here since its already a columnhide event. var me = this, editor = me.getEditor(); if (editor) { editor.onColumnHide(column); } }, /** * @private */ onColumnShow: function(ct, column) { // no isHeader check here since its already a columnshow event. var me = this, editor = me.getEditor(); if (editor) { editor.onColumnShow(column); } }, /** * @private */ onColumnMove: function(ct, column, fromIdx, toIdx) { // no isHeader check here since its already a columnmove event. var me = this, editor = me.getEditor(); // Inject field accessors on move because if the move FROM the main headerCt // and INTO a grouped header, the accessors will have been deleted but not added. // They are added conditionally. me.initFieldAccessors(column); if (editor) { // Must adjust the toIdx to account for removal if moving rightwards // because RowEditor.onColumnMove just calls Container.move which does not do this. editor.onColumnMove(column, fromIdx, toIdx); } }, /** * @private */ setColumnField: function(column, field) { var me = this, editor = me.getEditor(); if (editor) { // Remove the old editor and destroy it. editor.destroyColumnEditor(column); } me.callParent(arguments); if (editor) { editor.insertColumnEditor(column); } }, createColumnField: function(column, defaultField) { var editor = this.editor, def, field; if (editor) { def = editor.getDefaultFieldCfg(); } field = this.callParent([column, defaultField || def]); if (field) { field.skipLabelForAttribute = true; field.ariaAttributes = field.ariaAttributes || {}; if (this.grid.hideHeaders) { field.ariaAttributes['aria-label'] = column.text; } else { field.ariaAttributes['aria-labelledby'] = column.id; } } return field; }});