/** * A Grid header type which renders an icon, or a series of icons in a grid cell, and offers a scoped click * handler for each icon. * * @example * // Init the singleton. Any tag-based quick tips will start working. * Ext.tip.QuickTipManager.init(); * * Ext.create('Ext.data.Store', { * storeId:'employeeStore', * fields:['firstname', 'lastname', 'seniority', 'dep', 'hired'], * data:[ * {firstname:"Michael", lastname:"Scott"}, * {firstname:"Dwight", lastname:"Schrute"}, * {firstname:"Jim", lastname:"Halpert"}, * {firstname:"Kevin", lastname:"Malone"}, * {firstname:"Angela", lastname:"Martin"} * ] * }); * * Ext.create('Ext.grid.Panel', { * title: 'Action Column Demo', * store: Ext.data.StoreManager.lookup('employeeStore'), * columns: [ * {text: 'First Name', dataIndex:'firstname'}, * {text: 'Last Name', dataIndex:'lastname'}, * { * xtype:'actioncolumn', * width:50, * items: [{ * iconCls: 'x-fa fa-cog', * tooltip: 'Edit', * handler: function(grid, rowIndex, colIndex) { * var rec = grid.getStore().getAt(rowIndex); * alert("Edit " + rec.get('firstname')); * } * },{ * icon: 'extjs-build/examples/restful/images/delete.png', * tooltip: 'Delete', * handler: function(grid, rowIndex, colIndex) { * var rec = grid.getStore().getAt(rowIndex); * alert("Terminate " + rec.get('firstname')); * } * }] * } * ], * width: 250, * renderTo: Ext.getBody() * }); * * The action column can be at any index in the columns array, and a grid can have any number of * action columns. */Ext.define('Ext.grid.column.Action', { extend: 'Ext.grid.column.Column', alias: ['widget.actioncolumn'], alternateClassName: 'Ext.grid.ActionColumn', requires: [ 'Ext.grid.column.ActionProxy', 'Ext.Glyph' ], /** * @cfg {Number/String} glyph * @inheritdoc Ext.panel.Header#glyph * @since 6.2.0 */ /** * @cfg {String} [icon=Ext#BLANK_IMAGE_URL] * @inheritdoc Ext.panel.Header#icon */ /** * @cfg {String} iconCls * @inheritdoc Ext.panel.Header#cfg-iconCls * @localdoc **Note:** To determine the class dynamically, configure the Column with * a `{@link #getClass}` function. */ /** * @cfg {Function/String} handler * A function called when the icon is clicked. * @cfg {Ext.view.Table} handler.view The owning TableView. * @cfg {Number} handler.rowIndex The row index clicked on. * @cfg {Number} handler.colIndex The column index clicked on. * @cfg {Object} handler.item The clicked item (or this Column if multiple {@link #cfg-items} were not configured). * @cfg {Event} handler.e The click event. * @cfg {Ext.data.Model} handler.record The Record underlying the clicked row. * @cfg {HTMLElement} handler.row The table row clicked upon. * @controllable */ /** * @cfg {Object} scope * The scope (`this` reference) in which the `{@link #handler}`, * `{@link #getClass}`, `{@link #cfg-isDisabled}` and `{@link #getTip}` functions * are executed. * Defaults to this Column. */ /** * @cfg {String} tooltip * A tooltip message to be displayed on hover. {@link Ext.tip.QuickTipManager#init Ext.tip.QuickTipManager} must * have been initialized. * * The tooltip may also be determined on a row by row basis by configuring a {@link #getTip} method. */ /** * @cfg {Boolean} disabled * If true, the action will not respond to click events, and will be displayed semi-opaque. * * This Column may also be disabled on a row by row basis by configuring a {@link #cfg-isDisabled} method. */ /** * @cfg {Boolean} [stopSelection=true] * Prevent grid selection upon click. * Beware that if you allow for the selection to happen then the selection model will steal focus from * any possible floating window (like a message box) raised in the handler. This will prevent closing the * window when pressing the Escape button since it will no longer contain a focused component. */ stopSelection: true, /** * @cfg {Function} getClass * A function which returns the CSS class to apply to the icon image. * * For information on using the icons provided in the SDK see {@link #iconCls}. * @cfg {Object} getClass.v The value of the column's configured field (if any). * @cfg {Object} getClass.metadata An object in which you may set the following attributes: * @cfg {String} getClass.metadata.css A CSS class name to add to the cell's TD element. * @cfg {String} getClass.metadata.attr An HTML attribute definition string to apply to the data container * element *within* the table cell (e.g. 'style="color:red;"'). * @cfg {Ext.data.Model} getClass.r The Record providing the data. * @cfg {Number} getClass.rowIndex The row index. * @cfg {Number} getClass.colIndex The column index. * @cfg {Ext.data.Store} getClass.store The Store which is providing the data Model. * * @controllable */ /** * @cfg {Function} isDisabled A function which determines whether the action item for any row is disabled and returns `true` or `false`. * @cfg {Ext.view.Table} isDisabled.view The owning TableView. * @cfg {Number} isDisabled.rowIndex The row index. * @cfg {Number} isDisabled.colIndex The column index. * @cfg {Object} isDisabled.item The clicked item (or this Column if multiple {@link #cfg-items} were not configured). * @cfg {Ext.data.Model} isDisabled.record The Record underlying the row. * * @controllable */ /** * @cfg {Function} getTip A function which returns the tooltip string for any row. * * *Note*: Outside of an Ext.application() use of this config requires * {@link Ext.tip.QuickTipManager#init} to be called. * * Ext.tip.QuickTipManager.init(); * * Ext.create('Ext.data.Store', { * storeId: 'employeeStore', * fields: ['firstname', 'grade'], * data: [{ * firstname: "Michael", * grade: 50 * }, { * firstname: "Dwight", * grade: 100 * }] * }); * * Ext.create('Ext.grid.Panel', { * title: 'Action Column Demo', * store: Ext.data.StoreManager.lookup('employeeStore'), * columns: [{ * text: 'First Name', * dataIndex: 'firstname' * }, { * text: 'Last Name', * dataIndex: 'grade' * }, { * xtype: 'actioncolumn', * width: 50, * icon: 'sample/icons/action-icons.png', * getTip: function(value, metadata, record, row, col, store) { * var avg = store.average('grade'), * grade = record.get('grade'); * * if (grade < avg) { * metadata.tdCls = "below-average"; * } * * return grade > 70 ? 'Pass' : 'Fail'; * }, * handler: function(grid, rowIndex, colIndex) { * var rec = grid.getStore().getAt(rowIndex); * alert("Edit " + rec.get('firstname')); * } * }], * width: 250, * renderTo: document.body * }); * * @param {Object} value The value of the column's configured field (if any). * @param {Object} metadata An object in which you may set the following attributes: * @param {String} metadata.tdCls A CSS class name to add to the cell's TD element. * * metadata.tdCls = "custom-cell-cls"; * * @param {String} metadata.tdAttr An HTML attribute definition string to apply to * the data container element _within_ the table cell. * * metadata.tdCls = tdAttr = "*"; * // * see https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes * // be aware that setting cell attributes may override the cell layout * // provided by the framework * * @param {String} metadata.tdStyle An inline style for the table cell * * metadata.tdStyle = "background-color:red;"; * * @param {Ext.data.Model} record The Record providing the data. * @param {Number} rowIndex The row index. * @param {Number} colIndex The column index. * @param {Ext.data.Store} store The Store which is providing the data Model. * @return {String} tip The tip text * * @controllable */ /** * @cfg {Object[]} items * An Array which may contain multiple icon definitions, each element of which may contain: * * @cfg {String} items.icon The url of an image to display as the clickable element in the column. * * @cfg {String} items.iconCls A CSS class to apply to the icon element. To * determine the class dynamically, configure the item with a `getClass` function. * * @cfg {Number} items.tabIndex The tabIndex attribute value for the action item. If this * value is not defined, {@link #itemTabIndex} will be used instead. * * @cfg {String} items.ariaRole The ARIA role attribute value for the action item. If this * value is not defined, {@link #itemAriaRole} will be used instead. * * For information on using the icons provided in the SDK see {@link #iconCls}. * * @cfg {Function} items.getClass A function which returns the CSS class to apply to the icon image. * @cfg {Object} items.getClass.v The value of the column's configured field (if any). * @cfg {Object} items.getClass.metadata An object in which you may set the following attributes: * @cfg {String} items.getClass.metadata.css A CSS class name to add to the cell's TD element. * @cfg {String} items.getClass.metadata.attr An HTML attribute definition string to apply to the data * container element _within_ the table cell (e.g. 'style="color:red;"'). * @cfg {Ext.data.Model} items.getClass.r The Record providing the data. * @cfg {Number} items.getClass.rowIndex The row index. * @cfg {Number} items.getClass.colIndex The column index. * @cfg {Ext.data.Store} items.getClass.store The Store which is providing the data Model. * * @cfg {Function} items.handler A function called when the icon is clicked. * @cfg {Ext.view.Table} items.handler.view The owning TableView. * @cfg {Number} items.handler.rowIndex The row index clicked on. * @cfg {Number} items.handler.colIndex The column index clicked on. * @cfg {Object} items.handler.item The clicked item (or this Column if multiple {@link #cfg-items} were not configured). * @cfg {Event} items.handler.e The click event. * @cfg {Ext.data.Model} items.handler.record The Record underlying the clicked row. * @cfg {HTMLElement} items.row The table row clicked upon. * * @cfg {Function} items.isDisabled A function which determines whether the action item for any row is disabled and returns `true` or `false`. * @cfg {Ext.view.Table} items.isDisabled.view The owning TableView. * @cfg {Number} items.isDisabled.rowIndex The row index. * @cfg {Number} items.isDisabled.colIndex The column index. * @cfg {Object} items.isDisabled.item The clicked item (or this Column if multiple {@link #cfg-items} were not configured). * @cfg {Ext.data.Model} items.isDisabled.record The Record underlying the row. * * @cfg {Function} items.getTip A function which returns the tooltip string for any row. * @cfg {Object} items.getTip.v The value of the column's configured field (if any). * @cfg {Object} items.getTip.metadata An object in which you may set the following attributes: * @cfg {String} items.getTip.metadata.css A CSS class name to add to the cell's TD element. * @cfg {String} items.getTip.metadata.attr An HTML attribute definition string to apply to the data * container element _within_ the table cell (e.g. 'style="color:red;"'). * @cfg {Ext.data.Model} items.getTip.r The Record providing the data. * @cfg {Number} items.getTip.rowIndex The row index. * @cfg {Number} items.getTip.colIndex The column index. * @cfg {Ext.data.Store} items.getTip.store The Store which is providing the data Model. * * @cfg {Object} items.scope The scope (`this` reference) in which the `handler`, `getClass`, `isDisabled` and `getTip` functions * are executed. Fallback defaults are this Column's configured scope, then this Column. * * @cfg {String} items.tooltip A tooltip message to be displayed on hover. * {@link Ext.tip.QuickTipManager#init Ext.tip.QuickTipManager} must have been initialized. * * The tooltip may also be determined on a row by row basis by configuring a `getTip` method. * * @cfg {Boolean} items.disabled If true, the action will not respond to click events, and will be displayed semi-opaque. * * This item may also be disabled on a row by row basis by configuring an `isDisabled` method. */ /** * @property {Array} items * An array of action items copied from the configured {@link #cfg-items items} configuration. Each will have * an `enable` and `disable` method added which will enable and disable the associated action, and * update the displayed icon accordingly. */ actionIdRe: new RegExp(Ext.baseCSSPrefix + 'action-col-(\\d+)'), /** * @cfg {String} altText * The alt text to use for the image element. */ altText: '', /** * @cfg {String} [menuText=<i>Actions</i>] * Text to display in this column's menu item if no {@link #text} was specified as a header. */ menuText: '<i>Actions</i>', /** * @cfg {Number} [itemTabIndex=0] Default tabIndex attribute value for each action item. */ itemTabIndex: 0, /** * @cfg {String} [itemAriaRole="button"] Default ARIA role for each action item. */ itemAriaRole: 'button', maskOnDisable: false, // Disable means the action(s) ignoreExport: true, sortable: false, innerCls: Ext.baseCSSPrefix + 'grid-cell-inner-action-col', actionIconCls: Ext.baseCSSPrefix + 'action-col-icon', constructor: function(config) { var me = this, cfg = Ext.apply({}, config), // Items may be defined on the prototype items = cfg.items || me.items || [me], hasGetClass, i, len, item; me.origRenderer = cfg.renderer || me.renderer; me.origScope = cfg.scope || me.scope; me.renderer = me.scope = cfg.renderer = cfg.scope = null; // This is a Container. Delete the items config to be reinstated after construction. cfg.items = null; me.callParent([cfg]); // Items is an array property of ActionColumns me.items = items; for (i = 0, len = items.length; i < len; ++i) { item = items[i]; if (item.substr && item[0] === '@') { item = me.getAction(item.substr(1)); } if (item.isAction) { items[i] = item.initialConfig; // Register an ActinoProxy as a Component with the Action. // Action methods will be relayed down into the targeted item set. item.addComponent(new Ext.grid.column.ActionProxy(me, items[i], i)); } if (item.getClass) { hasGetClass = true; } } // Also need to check for getClass, since it changes how the cell renders if (me.origRenderer || hasGetClass) { me.hasCustomRenderer = true; } }, initComponent: function() { var me = this; me.callParent(); if (me.sortable && !me.dataIndex) { me.sortable = false; } }, // Renderer closure iterates through items creating an <img> element for each and tagging with an identifying // class name x-action-col-{n} defaultRenderer: function(v, cellValues, record, rowIdx, colIdx, store, view) { var me = this, scope = me.origScope || me, items = me.items, len = items.length, i, item, ret, disabled, tooltip, altText, icon, glyph, tabIndex, ariaRole; // Allow a configured renderer to create initial value (And set the other values in the "metadata" argument!) // Assign a new variable here, since if we modify "v" it will also modify the arguments collection, meaning // we will pass an incorrect value to getClass/getTip ret = Ext.isFunction(me.origRenderer) ? me.origRenderer.apply(scope, arguments) || '' : ''; cellValues.tdCls += ' ' + Ext.baseCSSPrefix + 'action-col-cell'; for (i = 0; i < len; i++) { item = items[i]; icon = item.icon; glyph = item.glyph; disabled = item.disabled || (item.isDisabled ? Ext.callback(item.isDisabled, item.scope || me.origScope, [view, rowIdx, colIdx, item, record], 0, me) : false); tooltip = item.tooltip || (item.getTip ? Ext.callback(item.getTip, item.scope || me.origScope, arguments, 0, me) : null); altText = item.getAltText ? Ext.callback(item.getAltText, item.scope || me.origScope, arguments, 0, me) : item.altText || me.altText; // Only process the item action setup once. if (!item.hasActionConfiguration) { // Apply our documented default to all items item.stopSelection = me.stopSelection; item.disable = Ext.Function.bind(me.disableAction, me, [i], 0); item.enable = Ext.Function.bind(me.enableAction, me, [i], 0); item.hasActionConfiguration = true; } // If the ActionItem is using a glyph, convert it to an Ext.Glyph instance so we can extract the data easily. if (glyph) { glyph = Ext.Glyph.fly(glyph); } // Pull in tabIndex and ariarRols from item, unless the item is this, in which case // that would be wrong, and the icon would get column header values. tabIndex = (item !== me && item.tabIndex !== undefined) ? item.tabIndex : me.itemTabIndex; ariaRole = (item !== me && item.ariaRole !== undefined) ? item.ariaRole : me.itemAriaRole; ret += '<' + (icon ? 'img' : 'div') + (typeof tabIndex === 'number' ? ' tabIndex="' + tabIndex + '"' : '') + (ariaRole ? ' role="' + ariaRole + '"' : ' role="presentation"') + (icon ? (' alt="' + altText + '" src="' + item.icon + '"') : '') + ' class="' + me.actionIconCls + ' ' + Ext.baseCSSPrefix + 'action-col-' + String(i) + ' ' + (disabled ? me.disabledCls + ' ' : ' ') + (item.hidden ? Ext.baseCSSPrefix + 'hidden-display ' : '') + (item.getClass ? Ext.callback(item.getClass, item.scope || me.origScope, arguments, undefined, me) : (item.iconCls || me.iconCls || '')) + '"' + (tooltip ? ' data-qtip="' + tooltip + '"' : '') + (icon ? '/>' : glyph ? (' style="font-family:' + glyph.fontFamily + '">' + glyph.character + '</div>') : '></div>'); } return ret; }, updater: function(cell, value, record, view, dataSource) { var cellValues = {}; Ext.fly(cell).addCls(cellValues.tdCls).down(this.getView().innerSelector, true).innerHTML = this.defaultRenderer(value, cellValues, record, null, null, dataSource, view); }, /** * Enables this ActionColumn's action at the specified index. * @param {Number/Ext.grid.column.Action} index * @param {Boolean} [silent=false] */ enableAction: function(index, silent) { var me = this; if (!index) { index = 0; } else if (!Ext.isNumber(index)) { index = Ext.Array.indexOf(me.items, index); } me.items[index].disabled = false; me.up('tablepanel').el.select('.' + Ext.baseCSSPrefix + 'action-col-' + index).removeCls(me.disabledCls); if (!silent) { me.fireEvent('enable', me); } }, /** * Disables this ActionColumn's action at the specified index. * @param {Number/Ext.grid.column.Action} index * @param {Boolean} [silent=false] */ disableAction: function(index, silent) { var me = this; if (!index) { index = 0; } else if (!Ext.isNumber(index)) { index = Ext.Array.indexOf(me.items, index); } me.items[index].disabled = true; me.up('tablepanel').el.select('.' + Ext.baseCSSPrefix + 'action-col-' + index).addCls(me.disabledCls); if (!silent) { me.fireEvent('disable', me); } }, doDestroy: function() { // Action column items property is an array, unlike the normal Container's MixedCollection. // If we don't null it here, parent doDestroy() can blow up. this.renderer = this.items = null; return this.callParent(); }, /** * @private * Process and re-fire events routed from the Ext.panel.Table's processEvent method. * Also fires any configured click handlers. By default, cancels the mousedown event to prevent selection. * Returns the event handler's status to allow canceling of GridView's bubbling process. */ processEvent: function(type, view, cell, recordIndex, cellIndex, e, record, row) { var me = this, target = e.getTarget(), key = type === 'keydown' && e.getKey(), match, item, disabled, cellFly = Ext.fly(cell); // Flag event to tell SelectionModel not to process it. e.stopSelection = !key && me.stopSelection; // If the target was not within a cell (ie it's a keydown event from the View), then // IF there's only one action icon, action it. If there is more than one, the user must // invoke actionable mode to navigate into the cell. if (key && (target === cell || !cellFly.contains(target))) { target = cellFly.query('.' + me.actionIconCls, true); if (target.length === 1) { target = target[0]; } else { return; } } // NOTE: The statement below tests the truthiness of an assignment. if (target && (match = target.className.match(me.actionIdRe))) { item = me.items[parseInt(match[1], 10)]; disabled = item.disabled || (item.isDisabled ? Ext.callback(item.isDisabled, item.scope || me.origScope, [view, recordIndex, cellIndex, item, record], 0, me) : false); if (item && !disabled) { // Do not allow focus to follow from this mousedown unless the grid is already in actionable mode if (type === 'mousedown' && !me.getView().actionableMode) { e.preventDefault(); } else if (type === 'click' || (key === e.ENTER || key === e.SPACE)) { Ext.callback(item.handler || me.handler, item.scope || me.origScope, [view, recordIndex, cellIndex, item, e, record, row], undefined, me); // Handler could possibly destroy the grid, so check we're still available. // // If the handler moved focus outside of the view, do not allow this event to propagate // to cause any navigation. if (view.destroyed) { return false; } else { // If the record was deleted by the handler, refresh // the position based upon coordinates. if (!e.position.getNode()) { e.position.refresh(); } if (!view.el.contains(Ext.Element.getActiveElement())) { return false; } } } } } return me.callParent(arguments); }, cascade: function(fn, scope) { fn.call(scope||this, this); }, // Private override because this cannot function as a Container, and it has an items property which is an Array, NOT a MixedCollection. getRefItems: function() { return []; }, privates: { getFocusables: function() { // Override is here to prevent the default behaviour which tries to access // this.items.items, which will be null. return []; }, // Overriden method to always return a bitwise value that will result in a call to this column's updater. shouldUpdateCell: function() { return 2; } }});