/** * This class specifies the definition for a column inside a {@link Ext.grid.Grid}. It * encompasses both the grid header configuration as well as displaying data within the * grid itself. * * In general an array of column configurations will be passed to the grid: * * @example * Ext.create('Ext.data.Store', { * storeId: 'employeeStore', * fields: ['firstname', 'lastname', 'seniority', 'dep', 'hired'], * data: [ * {firstname:"Michael", lastname:"Scott", seniority:7, dep:"Management", hired:"01/10/2004"}, * {firstname:"Dwight", lastname:"Schrute", seniority:2, dep:"Sales", hired:"04/01/2004"}, * {firstname:"Jim", lastname:"Halpert", seniority:3, dep:"Sales", hired:"02/22/2006"}, * {firstname:"Kevin", lastname:"Malone", seniority:4, dep:"Accounting", hired:"06/10/2007"}, * {firstname:"Angela", lastname:"Martin", seniority:5, dep:"Accounting", hired:"10/21/2008"} * ] * }); * * var grid = Ext.create('Ext.grid.Grid', { * title: 'Column Demo', * store: Ext.data.StoreManager.lookup('employeeStore'), * columns: [ * {text: 'First Name', dataIndex:'firstname'}, * {text: 'Last Name', dataIndex:'lastname'}, * {text: 'Hired Month', dataIndex:'hired', xtype:'datecolumn', format:'M'}, * {text: 'Department (Yrs)', xtype:'templatecolumn', tpl:'{dep} ({seniority})'} * ], * width: 400 * }); * Ext.ViewPort.add(grid); * * # Convenience Subclasses * * There are several column subclasses that provide default rendering for various data types * * - {@link Ext.grid.column.Boolean}: Renders for boolean values * - {@link Ext.grid.column.Date}: Renders for date values * - {@link Ext.grid.column.Number}: Renders for numeric values * * For more information about configuring cell content, see {@link Ext.grid.Grid}. * * # Setting Sizes * * The columns can be only be given an explicit width value. If no width is specified the * grid will automatically the size the column to 20px. * * # Header Options * * - {@link #text}: Sets the header text for the column * - {@link #sortable}: Specifies whether the column can be sorted by clicking the header * or using the column menu * * # Data Options * * - {@link #dataIndex}: The dataIndex is the field in the underlying {@link Ext.data.Store} * to use as the value for the column. * - {@link #renderer}: Allows the underlying store value to be transformed before being * displayed in the grid. */Ext.define('Ext.grid.column.Column', { extend: 'Ext.grid.HeaderContainer', alternateClassName: 'Ext.grid.column.Template', xtype: ['gridcolumn', 'column', 'templatecolumn'], /** * @property {Boolean} isGridColumn * Set in this class to identify, at runtime, instances which are not instances of the * HeaderContainer base class, but are in fact, the subclass: Ext.grid.Column. */ isGridColumn: true, mixins: [ // This mixin is used to cache the padding size for cells in this column, // to be shared by all cells in the column. 'Ext.mixin.StyleCacher', 'Ext.mixin.Toolable' ], /** * @property {Boolean} isLeafHeader * This will be set to `true` if the column has no child columns. */ /** * @property {Boolean} isHeaderGroup * This will be set to `true` if the column has child columns. */ config: { /** * @cfg {String} [align='left'] * Sets the alignment of the header and rendered columns. * Possible values are: `'left'`, `'center'`, and `'right'`. */ align: undefined, // undefined so applier will run to determine default value /** * @cfg {Object} cell * The config object used to create {@link Ext.grid.cell.Base cells} for this column. * By default, cells use the {@link Ext.grid.cell.Cell gridcell} `xtype`. To create * a different type of cell, simply provide this config and the desired `xtype`. */ cell: { xtype: 'gridcell' }, /** * @cfg {String} dataIndex * The name of the field in the grid's {@link Ext.data.Store}'s {@link Ext.data.Model} definition from * which to draw the column's value. **Required.** */ dataIndex: null, /** * @cfg {Number} defaultWidth * A width to apply if the {@link #flex} or {@link #width} configurations have not * been specified. * * @since 6.2.0 */ defaultWidth: 100, emptyText: { cached: true, $value: '\xA0' }, /** * @cfg {String} text * The header text to be used as innerHTML (html tags are accepted) to display in the Grid. * **Note**: to have a clickable header with no text displayed you can use the default of ` ` aka ` `. */ text: '\xa0', /** * @cfg {Boolean} sortable * False to disable sorting of this column. Whether local/remote sorting is used is specified in * `{@link Ext.data.Store#remoteSort}`. */ sortable: true, /** * @cfg {Boolean} groupable * If the grid is {@link Ext.grid.Grid#grouped grouped}, and uses a * {@link Ext.grid.plugin.ViewOptions ViewOptions} plugin this option may be used to * disable the option to group by this column. By default, the group option is enabled. */ groupable: true, /** * @cfg {Boolean} resizable * False to prevent the column from being resizable. * Note that this configuration only works when the {@link Ext.grid.plugin.ColumnResizing ColumnResizing} plugin * is enabled on the {@link Ext.grid.Grid Grid}. */ resizable: true, /** * @cfg {Boolean} hideable * False to prevent the user from hiding this column. * * @since 6.5.0 */ hideable: true, /** * @cfg {Function/String} renderer * A renderer is a method which can be used to transform data (value, appearance, etc.) * before it is rendered. * * For example: * * { * text: 'Some column', * dataIndex: 'fieldName', * * renderer: function (value, record) { * if (value === 1) { * return '1 person'; * } * return value + ' people'; * } * } * * If a string is supplied, it should be the name of a renderer method from the * appropriate {@link Ext.app.ViewController}. * * This config is only processed if the {@link #cell} type is the default of * {@link Ext.grid.cell.Cell gridcell}. * * **Note** See {@link Ext.grid.Grid} documentation for other, better alternatives * to rendering cell content. * * @cfg {Object} renderer.value The data value for the current cell. * @cfg {Ext.data.Model} renderer.record The record for the current row. * @cfg {Number} renderer.dataIndex The dataIndex of the current column. * @cfg {Ext.grid.cell.Base} renderer.cell The current cell. * @cfg {Ext.grid.column.Column} renderer.column The current column. * @cfg {String} renderer.return The HTML string to be rendered. *Note*: to * render HTML into the cell, you will have to configure the column's {@link #cell} * with `encodeHtml: false` */ renderer: null, /** * @cfg {String} formatter * This config accepts a format specification as would be used in a `Ext.Template` * formatted token. For example `'round(2)'` to round numbers to 2 decimal places * or `'date("Y-m-d")'` to format a Date. * * In previous releases the `renderer` config had limited abilities to use one * of the `Ext.util.Format` methods but `formatter` now replaces that usage and * can also handle formatting parameters. * * When the value begins with `"this."` (for example, `"this.foo(2)"`), the * implied scope on which "foo" is found is the `scope` config for the column. * * If the `scope` is not given, or implied using a prefix of `"this"`, then either the * {@link #method-getController ViewController} or the closest ancestor component configured * as {@link #defaultListenerScope} is assumed to be the object with the method. * @since 6.2.0 */ formatter: null, /** * @cfg {Object} scope * The scope to use when calling the {@link #renderer} or {@link #formatter} function. */ scope: null, /** * @cfg {Boolean} editable * Set this to true to make this column editable. * Only applicable if the grid is using an {@link Ext.grid.plugin.Editable Editable} plugin. */ editable: null, /** * @cfg {Object/String} editor * An optional xtype or config object for a {@link Ext.field.Field Field} to use for editing. * Only applicable if the grid is using an {@link Ext.grid.plugin.Editable Editable} plugin. * Note also that {@link #editable} has to be set to true if you want to make this column editable. * If this configuration is not set, and {@link #editable} is set to true, the {@link #defaultEditor} is used. */ editor: null, /** * @cfg {Object/Ext.field.Field} * An optional config object that should not really be modified. This is used to create * a default editor used by the {@link Ext.grid.plugin.Editable Editable} plugin when no * {@link #editor} is specified. */ defaultEditor: { lazy: true, $value: { xtype: 'textfield', required: true } }, /** * @cfg {Boolean} ignore * Setting to `true` prevents this column from being used by plugins such as * {@link Ext.grid.plugin.ViewOptions} or {@link Ext.grid.plugin.Summary}. It is * intended for special columns such as the row number or checkbox selection. */ ignore: false, /** * @cfg {Boolean} ignoreExport * This flag indicates that this column will be ignored when grid data is exported. * * When grid data is exported you may want to export only some columns that are important * and not everything. You can set this flag on any column that you want to be ignored during export. * * This is used by {@link Ext.grid.plugin.Exporter exporter plugin}. */ ignoreExport: false, /** * @cfg {Ext.exporter.file.Style/Ext.exporter.file.Style[]} exportStyle * * A style definition that is used during data export via the {@link Ext.grid.plugin.Exporter exporter plugin}. * This style will be applied to the columns generated in the exported file. * * You could define it as a single object that will be used by all exporters: * * { * xtype: 'numbercolumn', * dataIndex: 'price', * exportStyle: { * format: 'Currency', * alignment: { * horizontal: 'Right' * }, * font: { * italic: true * } * } * } * * You could also define it as an array of objects, each object having a `type` that specifies by * which exporter will be used: * * { * xtype: 'numbercolumn', * dataIndex: 'price', * exportStyle: [{ * type: 'html', // used by the `html` exporter * format: 'Currency', * alignment: { * horizontal: 'Right' * }, * font: { * italic: true * } * },{ * type: 'csv', // used by the `csv` exporter * format: 'General' * }] * } * * Or you can define it as an array of objects that has: * * - one object with no `type` key that is considered the style to use by all exporters * - objects with the `type` key defined that are exceptions of the above rule * * { * xtype: 'numbercolumn', * dataIndex: 'price', * exportStyle: [{ * // no type defined means this is the default * format: 'Currency', * alignment: { * horizontal: 'Right' * }, * font: { * italic: true * } * },{ * type: 'csv', // only the CSV exporter has a special style * format: 'General' * }] * } * */ exportStyle: null, /** * @cfg {Boolean/Function/String} exportRenderer * * During data export via the {@link Ext.grid.plugin.Exporter} plugin the data for * this column could be formatted in multiple ways: * * - using the `exportStyle.format` * - using the `formatter` if no `exportStyle` is defined * - using the `exportRenderer` * * If you want to use the `renderer` defined on this column then set `exportRenderer` * to `true`. Beware that this should only happen if the `renderer` deals only with * data on the record or value and it does NOT style the cell or returns an html * string. * * { * xtype: 'numbercolumn', * dataIndex: 'price', * text: 'Price', * renderer: function (value, record, dataIndex, cell, column) { * return Ext.util.Format.currency(value); * }, * exportRenderer: true * } * * If you don't want to use the `renderer` during export but you still want to format * the value in a special way then you can provide a function to `exportRenderer` or * a string (which is a function name on the ViewController). * The provided function has the same signature as the renderer. * * { * xtype: 'numbercolumn', * dataIndex: 'price', * text: 'Price', * exportRenderer: function (value, record, dataIndex, cell, column) { * return Ext.util.Format.currency(value); * } * } * * * { * xtype: 'numbercolumn', * dataIndex: 'price', * text: 'Price', * exportRenderer: 'exportAsCurrency' // this is a function on the ViewController * } * * * If `exportStyle.format`, `formatter` and `exportRenderer` are all defined on the * column then the `exportStyle` wins and will be used to format the data for this * column. */ exportRenderer: false, /** * @cfg {String} summary * This config replaces the default mechanism of acquiring a summary result from * the summary record. When specified, this string is the name of a summary type: * * - {@link Ext.data.summary.Average average} * - {@link Ext.data.summary.Count count} * - {@link Ext.data.summary.Max max} * - {@link Ext.data.summary.Min min} * - {@link Ext.data.summary.Sum sum} * * The summary is based on either the {@link #cfg!summaryDataIndex} or the * {@link #cfg!dataIndex} if there is no `summaryDataIndex`. * * This config is only valid when all data is available client-side to calculate * summaries. * * It is generally best to allow the summary {@link Ext.data.Model record} to * computer summary values (and not use this config). In some cases, however, * this config can be useful to isolate summary calculations to only certain grids. * * To implement a custom summary for a column, use {@link #cfg!summaryRenderer}. * @since 6.5.0 */ summary: null, /** * @cfg {Object} summaryCell * The config object used to create {@link Ext.grid.cell.Base cells} in * {@link Ext.grid.SummaryRow Summary Rows} for this column. */ summaryCell: null, /** * @cfg {String} summaryDataIndex * For {@link Ext.grid.SummaryRow summary rows} this config overrides the normal * `dataIndex` to use from the summary record. * @since 6.5.0 */ summaryDataIndex: null, /** * @cfg {String} summaryFormatter * This summaryFormatter is similar to {@link #formatter} but is called before * displaying a value in the SummaryRow. The config is optional, if not specified * the default calculated value is shown. The summaryFormatter is called with: * * - value: The calculated value. * * Note that this configuration only works when the grid has the * {@link Ext.grid.plugin.Summary gridsummary} plugin enabled. */ summaryFormatter: null, /** * @cfg {Function/String} summaryRenderer * This summaryRenderer is called to render the value to display in a cell of a * summary row. If the value of this config is a String, it is the name of the * renderer method on the associated {@link Ext.Component#controller controller}. * * @cfg {Mixed} summaryRenderer.value The summary value to render. This value is * retrieved from the summary record based on the {@link #cfg!summaryDataIndex} or * {@link #cfg!dataIndex}, or by applying the {@link #cfg!summary} algorithm to * the appropriate records. While this value can be useful, it can also be ignored * and the renderer method can use the `context` information to determine the value * to render entirely on its own. * * @cfg {Object} summaryRenderer.context The summary context object. * * @cfg {String} summaryRenderer.context.dataIndex The data field. This will be * either the {@link #cfg!summaryDataIndex} if one is specified, or the normal * {@link #cfg!dataIndex} if not. * * @cfg {String} summaryRenderer.context.group The {@link Ext.data.Group group} * being summarized. This is `null` if the summary is for the whole `store`. * * @cfg {String} summaryRenderer.context.store The {@link Ext.data.Store store} * being summarized. * * If this method returns `undefined`, no update is made to the cell. Instead it * is assumed that the `summaryRenderer` has made all of the necessary changes. * * Note that this configuration only works when the grid has the * {@link Ext.grid.plugin.Summary gridsummary} plugin enabled. */ summaryRenderer: null, /** * @cfg {String/Function} summaryType * This configuration specifies the type of summary. There are several built in * summary types. These call underlying methods on the store: * * - {@link Ext.data.Store#count count} * - {@link Ext.data.Store#sum sum} * - {@link Ext.data.Store#min min} * - {@link Ext.data.Store#max max} * - {@link Ext.data.Store#average average} * * Any other name is assumed to be the name of a method on the associated * {@link Ext.app.ViewController view controller}. * * Note that this configuration only works when the grid has the * {@link Ext.grid.plugin.Summary gridsummary} plugin enabled. * * @deprecated 6.5 Use {@link #cfg!summary} or {@link #cfg!summaryRenderer} instead. */ summaryType: null, /** * @cfg {Boolean/Function/String} exportSummaryRenderer * * This config is similar to {@link #exportRenderer} but is applied to summary * records. */ exportSummaryRenderer: false, minWidth: 40, /** * @cfg {String/String[]/Ext.XTemplate} tpl * An {@link Ext.XTemplate XTemplate}, or an XTemplate *definition string* to use * to process a {@link Ext.data.Model records} data to produce a cell's rendered * value. * * @example * Ext.create('Ext.data.Store', { * storeId:'employeeStore', * fields:['firstname', 'lastname', 'seniority', 'department'], * groupField: 'department', * data:[ * { firstname: "Michael", lastname: "Scott", seniority: 7, department: "Management" }, * { firstname: "Dwight", lastname: "Schrute", seniority: 2, department: "Sales" }, * { firstname: "Jim", lastname: "Halpert", seniority: 3, department: "Sales" }, * { firstname: "Kevin", lastname: "Malone", seniority: 4, department: "Accounting" }, * { firstname: "Angela", lastname: "Martin", seniority: 5, department: "Accounting" } * ] * }); * * Ext.create('Ext.grid.Panel', { * title: 'Column Template Demo', * store: Ext.data.StoreManager.lookup('employeeStore'), * columns: [{ * text: 'Full Name', * tpl: '{firstname} {lastname}' * }, { * text: 'Department (Yrs)', * tpl: '{department} ({seniority})' * }], * height: 200, * width: 300, * renderTo: Ext.getBody() * }); * * This config is only processed if the {@link #cell} type is the default of * {@link Ext.grid.cell.Cell gridcell}. * * **Note** See {@link Ext.grid.Grid} documentation for other, better alternatives * to rendering cell content. */ tpl: null, /** * @cfg {Number} computedWidth * The computed width for this column, may come from either * {@link #width} or {@link #flex}. * @readonly */ computedWidth: null, /** * @cfg {Function/String/Object/Ext.util.Grouper} grouper * A grouper config object to apply when the standard grouping user interface is * is invoked. This option is, for example, available in the column's header * menu. * * Note that a grouper may also be specified as a function which accepts two * records to compare. * * A `{@link Ext.app.ViewController controller}` method can be used like so: * * grouper: 'groupMethodName' * * This is different then a `sorter` in that the `grouper` method is used to * set the {@link Ext.util.Grouper#cfg!groupFn groupFn}. This string returned * by this method is used to determine group membership. To specify both the * `grpoupFn` and the `sorterFn`: * * grouper: { * groupFn: 'groupMethodName' * sorterFn: 'sorterMethodName * } * * @since 6.5.0 */ grouper: { lazy: true, $value: null }, /** * @cfg {String/String[]/Ext.XTemplate} groupHeaderTpl * This config allows a column to replace the default template supplied by the * grid's {@link Ext.grid.RowHeader#tpl groupHeader.tpl}. * * @since 6.5.0 */ groupHeaderTpl: null, /** * @cfg {Function/String/Object/Ext.util.Sorter} sorter * A sorter config object to apply when the standard sort user interface is * is invoked. This is usually clicking this column header, but there are also * menu options to sort ascending or descending. * * Note that a sorter may also be specified as a function which accepts two * records to compare. * * A `{@link Ext.app.ViewController controller}` method can be used like so: * * sorter: 'sorterMethodName' * * Or more explicitly: * * sorter: { * sorterFn: 'sorterMethodName' * } * * By default sorting is based on the `dataIndex` but this can be adjusted * like so: * * sorter: { * property: 'otherProperty' * } * * @since 6.5.0 */ sorter: { lazy: true, $value: null }, /** * @cfg {Ext.grid.cell.Cell/Object} scratchCell * @since 6.5.0 * @private */ scratchCell: { lazy: true, $value: true }, /** * @cfg {Ext.menu.Menu/Object} menu * An optional menu configuration object which is merged with the grid's * {@link #cfg!columnMenu} to create this column's header menu. This can be set * to `null` to remove the menu from this column. To dynamically change whether * the menu should be enabled or not use the `menuDisabled` config. * * The grid's {@link Ext.grid.Grid#cfg!columnMenu} provides the sort items, this * config can be used to add column-specific menu items or override aspects of * the common items. * @since 6.5.0 */ menu: { lazy: true, $value: {} }, /** * @cfg {Boolean} [menuDisabled=false] * Set to `true` to disable this column's `menu` containing sort/hide options. * This can be useful if the menu will be dynamically available since setting * `menu` to `null` will eliminate the menu making dynamic changes to its * availability more expensive. * @since 6.5.0 */ menuDisabled: null, /** * @cfg {Ext.menu.CheckItem/Object} hideShowMenuItem * The {@link Ext.menu.CheckItem menu item} to be used by the owning grid's * header menu to hide or show this column. * @since 6.5.0 * @private */ hideShowMenuItem: { lazy: true, $value: { xtype: 'menucheckitem' } } }, toolDefaults: { ui: 'gridcolumn', zone: 'tail' }, toolAnchorName: 'titleWrapElement', dockTools: false, scrollable: false, docked: null, sortState: null, // These are not readable descriptions; the values go in the aria-sort attribute. ariaSortStates: { ASC: 'ascending', DESC: 'descending' }, inheritUi: true, classCls: Ext.baseCSSPrefix + 'gridcolumn', sortedCls: Ext.baseCSSPrefix + 'sorted', resizableCls: Ext.baseCSSPrefix + 'resizable', groupCls: Ext.baseCSSPrefix + 'group', leafCls: Ext.baseCSSPrefix + 'leaf', menuOpenCls: Ext.baseCSSPrefix + 'menu-open', alignCls: { left: Ext.baseCSSPrefix + 'align-left', center: Ext.baseCSSPrefix + 'align-center', right: Ext.baseCSSPrefix + 'align-right' }, /** * @event columnmenucreated * @member Ext.grid.Grid * Fired when a column first creates its column menu. This is to allow plugins * to access and manipulate the column menu. * * There will be the two sort items, and a column hide/show item with a child menu of * checkboxes. After this, developers may add custom enu items. * * Menu items may be configured with a `weight` config, and those with the lowest weight * gravitate to the top. * * The sort ascending, sort descending, and hide columns items have weight -3, -2, and -1 * @param {Ext.grid.Grid} grid This Grid * @param {Ext.grid.Column} column The column creating the menu * @param {Ext.menu.Menu} menu The column's new menu */ constructor: function (config) { var me = this, isHeaderGroup, menu; // If we are configured or prototyped as a HeaderGroup // TODO - move to updater (me.columns won't work in all cases) if (config.columns || me.columns) { isHeaderGroup = me.isHeaderGroup = true; } else { me.isLeafHeader = true; } me.callParent([config]); me.addCls(isHeaderGroup ? me.groupCls : me.leafCls); menu = me.getConfig('menu', /*peek=*/true); if (!menu && me.getMenuDisabled() === null) { me.setMenuDisabled(true); } }, getTemplate: function () { var me = this, beforeTitleTemplate = me.beforeTitleTemplate, afterTitleTemplate = me.afterTitleTemplate, titleTpl = []; // Hook for subclasses to insert extra elements if (beforeTitleTemplate) { titleTpl.push.apply(titleTpl, beforeTitleTemplate); } titleTpl.push({ reference: 'titleElement', className: Ext.baseCSSPrefix + 'title-el', children: [{ reference: 'textElement', className: Ext.baseCSSPrefix + 'text-el' }, { reference: 'sortIconElement', cls: Ext.baseCSSPrefix + 'sort-icon-el ' + Ext.baseCSSPrefix + 'font-icon' }] }); // Hook for subclasses to insert extra elements if (afterTitleTemplate) { titleTpl.push.apply(titleTpl, afterTitleTemplate); } return [{ reference: 'headerElement', cls: Ext.baseCSSPrefix + 'header-el', children: [{ reference: 'titleWrapElement', cls: Ext.baseCSSPrefix + 'title-wrap-el', uiCls: 'title-wrap-el', children: titleTpl }, { reference: 'resizerElement', cls: Ext.baseCSSPrefix + 'resizer-el ' + Ext.baseCSSPrefix + 'item-no-tap' }, { reference: 'triggerElement', cls: Ext.baseCSSPrefix + 'trigger-el ' + Ext.baseCSSPrefix + 'font-icon ' + Ext.baseCSSPrefix + 'item-no-tap' }] }, { reference: 'bodyElement', cls: Ext.baseCSSPrefix + 'body-el', uiCls: 'body-el' }]; }, onAdded: function(parent, instanced) { this.visibleIndex = null; this.callParent([parent, instanced]); }, /** * Returns the index of this column in the list of *visible* columns only if this column is a base level Column. If it * is a group column, it returns `false`. * @return {Number} */ getVisibleIndex: function() { // Note that the visibleIndex property is assigned by the owning HeaderContainer // when assembling the visible column set for the view. var visibleIndex = this.visibleIndex, rootHeaders; if (visibleIndex == null) { if (this.isHeaderGroup) { visibleIndex = false; } else { rootHeaders = this.getRootHeaderCt(); if (rootHeaders) { visibleIndex = rootHeaders.indexOfLeaf(this); } } this.visibleIndex = visibleIndex; } return visibleIndex; }, _columnScopeRe: /^column\./, _gridScopeRe: /^grid\./, applyMenu: function (menu) { var me = this, grid = me.getGrid(), columnScopeRe = me._columnScopeRe, gridScopeRe = me._gridScopeRe, extraItems, gridColumnMenu, i, item, items, s; // Allow menu:null to rid the column of all menus... so only merge in the // grid's column menu if we have a non-null menu if (menu && !menu.isMenu) { if (Ext.isArray(menu)) { extraItems = menu; menu = null; } else if (!menu.items) { menu = { items: menu }; } if (!(gridColumnMenu = grid.getColumnMenu())) { // if menu was an array it is now null, so just make an empty {} menu = menu ? Ext.clone(menu) : {}; } else { gridColumnMenu = Ext.clone(gridColumnMenu); menu = menu ? Ext.merge(gridColumnMenu, menu) : gridColumnMenu; } menu.ownerCmp = me; menu = Ext.create(menu); // We cannot use defaultListenerScope to map handlers in our menu to // ourselves because user views would then be blocked from doing so to // items they may have added to the same menu. // // Our trick is to encode special scopes in the handler names and see // if they have survived until now. It is possible the user has set // the handler to something else... for (items = menu.getItems().items, i = items && items.length; i-- > 0; ) { item = items[i]; if (columnScopeRe.test(s = item.getHandler() || '')) { item.setHandler(s.substr(7)); // remove "column." item.scope = me; } else if (gridScopeRe.test(s)) { item.setHandler(s.substr(5)); // remove "grid." item.scope = grid; } else if (item.isMenuCheckItem) { if (columnScopeRe.test(s = item.getCheckHandler() || '')) { item.setCheckHandler(s.substr(7)); item.scope = me; } else if (gridScopeRe.test(s)) { item.setCheckHandler(s.substr(5)); item.scope = grid; } } } if (extraItems) { menu.add(extraItems); } grid.fireEvent('columnmenucreated', grid, me, menu); } return menu; }, beforeShowMenu: function (menu) { var me = this, grid = me.getGrid(), grouper = grid.getStore().getGrouper(), groupByThis = menu.getComponent('groupByThis'), showInGroups = menu.getComponent('showInGroups'), sortAsc = menu.getComponent('sortAsc'), sortDesc = menu.getComponent('sortDesc'), sortable = this.isSortable(), groupedByThis = false, dataIndex = me.getDataIndex(); if (sortAsc) { sortAsc.setDisabled(!sortable); } if (sortDesc) { sortDesc.setDisabled(!sortable); } if (groupByThis) { if (grouper) { if (!(groupedByThis = grouper === me.getGrouper())) { groupedByThis = dataIndex != null && dataIndex === grouper.getProperty(); } } groupByThis.setChecked(groupedByThis); groupByThis.setDisabled(groupedByThis); } if (showInGroups) { showInGroups.setChecked(!!grouper); showInGroups.setDisabled(!grouper); } }, showMenu: function () { var me = this, menu = !me.getMenuDisabled() && me.getMenu(), menuOpenCls = me.menuOpenCls, columnsMenu, grid; // Only try if the menu is not disabled, and there *is* a menu if (menu) { grid = me.getGrid(); columnsMenu = grid.getColumnsMenuItem(); menu.add(columnsMenu); if (me.beforeShowMenu(menu) !== false && grid.beforeShowColumnMenu(me, menu) !== false) { menu.showBy(me.triggerElement); // Add menu open class which shows the trigger element while the menu is open me.addCls(menuOpenCls); menu.on({ single: true, hide: function () { if (!me.destroyed) { me.removeCls(menuOpenCls); menu.remove(columnsMenu, /*destroy=*/false); } } }); } } }, getCells: function () { var cells = [], rows = this.getGrid().items.items, len = rows.length, i, row; for (i = 0; i < len; ++i) { row = rows[i]; if (row.isGridRow) { cells.push(row.getCellByColumn(this)); } } return cells; }, getColumnForField: function (fieldName) { if (fieldName === this.getDataIndex()) { return this; } return this.callParent([ fieldName ]); }, applyTpl: function (tpl) { return Ext.XTemplate.get(tpl); }, applyAlign: function(align, oldAlign) { if (align == null) { align = this.isHeaderGroup ? 'center' : 'left'; } return align; }, updateAlign: function (align, oldAlign) { var me = this, alignCls = me.alignCls; if (oldAlign) { me.removeCls(alignCls[oldAlign]); } if (align) { //<debug> if (!alignCls[align]) { Ext.raise("Invalid value for align: '" + align + "'"); } //</debug> me.addCls(alignCls[align]); } me.syncToolableAlign(); }, updateMenuDisabled: function (menuDisabled) { if (this.triggerElement) { this.triggerElement.setVisible(!menuDisabled); } }, initialize: function () { var me = this; if (me.isLeafHeader && !me.getWidth() && me.getFlex() == null) { me.setWidth(me.getDefaultWidth()); } me.callParent(); me.element.on({ tap: 'onColumnTap', longpress: 'onColumnLongPress', scope: this }); me.triggerElement.on({ tap: 'onTriggerTap', scope: this }); me.resizerElement.on({ tap: 'onResizerTap', scope: this }); if (me.isHeaderGroup) { me.on({ add: 'doVisibilityCheck', remove: 'doVisibilityCheck', show: 'onColumnShow', hide: 'onColumnHide', delegate: '> column', scope: me }); me.on({ show: 'onShow', scope: me }); } }, onColumnTap: function (e) { var me = this; // Tapping on the trigger or resizer must not sort the column and // neither should tapping on any components (e.g. tools) contained // in the column. if (Ext.Component.from(e) !== me || e.getTarget('.' + Ext.baseCSSPrefix + 'item-no-tap', me)) { return; } // HeaderContainer's sortable config must be honoured dynamically since // SelectionModels can change it. if (me.getRootHeaderCt().getSortable()) { me.toggleSortState(); } return me.fireEvent('tap', me, e); }, onTriggerTap: function (e) { this.fireEvent('triggertap', this, e); }, onResizerTap: function (e) { // If they tapped on the resizer without dragging, interpret that as a tap // on the trigger, if it's in the correct region. if (e.getPoint().isContainedBy(this.triggerElement.getRegion())) { this.fireEvent('triggertap', this, e); } }, onColumnLongPress: function (e) { this.fireEvent('longpress', this, e); }, onGroupByThis: function () { var me = this, grid = me.getGrid(), grouper = me.getGrouper(), store = grid.getStore(), dataIndex; if (!grouper) { dataIndex = me.getDataIndex(); if (dataIndex != null) { me.setGrouper({ property: dataIndex }); grouper = me.getGrouper(); } } if (grouper) { store.setGrouper(grouper); } }, onSortDirectionToggle: function (menuItem) { var me = this, grid = me.getGrid(), store = grid.getStore(), sorter = me.getSorter(), sorters = store.getSorters(), isSorted = sorter && (sorters.contains(sorter) || sorter === store.getGrouper()), direction = menuItem.direction; // Remove sorter on uncheck if its the matching direction if (sorter && sorter.getDirection() === direction) { sorters.remove(sorter); // Store will not refresh in response to having a sorter removed, so we must // clear the column header arrow now. me.setSortState(null); } else { // If have no sorter, or store is not sorting by that sorter, or the sorter // is opposite to what we just checked then sort according to the CheckItems's // direction if (!isSorted || sorter.getDirection() !== direction) { me.sort(direction); } } }, onToggleShowInGroups: function (menuItem) { if (!menuItem.getChecked()) { var grid = this.getGrid(), store = grid.getStore(); store.setGrouper(null); } }, updateResizable: function (resizable) { var me = this, widthed = me.getWidth() != null, flexed = me.getFlex() != null; // Column only drag-resizable if it's widthed, flexed, or a leaf. // If it's shrinkwrapping child columns then the child columns must be resized. me.toggleCls(me.resizableCls, !!(me.getResizable() && (widthed || flexed || me.isLeafHeader))); }, updateText: function (text) { this.setHtml(text || '\xa0'); }, onResize: function () { // Update the resizability of this column based on *how* it's just been sized. // If we are shrinkwrapping, we are not drag-resizable. this.updateResizable(this.getResizable()); // Computed with needs to be exact so that sub-pixel changes are // not rejected by the config system because scrollbars may // depend upon the *exact* width of the cells in the view. this.measureWidth(); }, getComputedWidth: function () { return this.isVisible(true) ? this._computedWidth : 0; }, updateColumns: function (columns) { this.getItems(); this.add(columns); }, measureWidth: function () { // Computed width must be a real. exact pixel width. // It cannot be em or rem etc because it is used to size owned cells // and different styles and fonts may be applied to cells. var width = this.el.measure('w'); this.setComputedWidth(width); return width; }, updateComputedWidth: function (value, oldValue) { var me = this, rootHeaderCt = !me.isConfiguring && me.getRootHeaderCt(); // This is how grid's resize their cells in response. Not through events. // Width change events arrive asynchronously through resize listeners // and that would cause janky grid resizes. // // By informing the grid, it can force all flexed columns to republish // their computed widths, and correctly update all cells in one pass. if (rootHeaderCt) { // This updates the cells. rootHeaderCt.onColumnComputedWidthChange(me, value); // Fire the event after cells have been resized me.fireEvent('columnresize', me, value, oldValue); } }, updateDataIndex: function (dataIndex) { if (this.isConfiguring) { return; } var editor = this.getEditor(); if (editor) { editor.name = dataIndex; } else { this.getDefaultEditor().name = dataIndex; } }, applyGroupHeaderTpl: function (tpl) { return Ext.XTemplate.get(tpl); }, updateGroupHeaderTpl: function (tpl) { var grouper = this.grouper; if (grouper) { grouper.headerTpl = tpl; } }, isSortable: function () { return this.isLeafHeader && this.getSortable() && this.getGrid().sortableColumns !== false; }, applyEditor: function (value) { if (value && !value.isInstance) { if (typeof(value) === 'string') { value = { xtype: value }; } if (!value.xtype) { value = Ext.apply({ xtype: value.field ? 'celleditor' : 'textfield' }, value); } value.name = value.name || this.getDataIndex(); return Ext.create(value); } return value; }, updateDefaultEditor: function(editor) { if (!editor.name) { editor.name = this.getDataIndex(); } }, updateEditor: function (editor, oldEditor) { // If we are changing editors destroy the last one // but if we are changing from a field to a cell editor make sure we do not destroy // the field that is now a child of the cell editor if (oldEditor && (!editor || (editor.isCellEditor && editor.getField() !== oldEditor))) { oldEditor.destroy(); } }, applyFormatter: function (format) { var me = this, fmt = format, parser; if (fmt) { parser = Ext.app.bind.Parser.fly(fmt); fmt = parser.compileFormat(); parser.release(); return function (v) { return fmt(v, me.getScope() || me.resolveListenerScope()); }; } return fmt; }, applySummaryFormatter: function (format) { var me = this, fmt = format, parser; if (fmt) { parser = Ext.app.bind.Parser.fly(fmt); fmt = parser.compileFormat(); parser.release(); return function (v) { return fmt(v, me.getScope() || me.resolveListenerScope()); }; } return fmt; }, applyGrouper: function (grouper) { var me = this, cfg = grouper; if (cfg && !cfg.isInstance) { if (typeof cfg === 'string') { cfg = { groupFn: cfg }; } else { cfg = Ext.apply({}, cfg); } if (typeof cfg.groupFn === 'string') { cfg = me.scopeReplacer(cfg, grouper, 'groupFn', 'setGroupFn'); } if (typeof cfg.sorterFn === 'string') { cfg = me.scopeReplacer(cfg, grouper, 'sorterFn', 'setSorterFn'); } grouper = new Ext.util.Grouper(cfg); } // The owner/headerTpl expandos on our grouper are picked up by the ItemHeader // as a means to override the list's groupHeaderTpl... if (grouper) { grouper.owner = me.getGrid(); grouper.headerTpl = me.getGroupHeaderTpl(); } return grouper; }, updateGrouper: function (grouper, oldGrouper) { var store = this.getGrid().getStore(); if (store && oldGrouper) { if (oldGrouper === store.getGrouper()) { store.setGrouper(grouper); } } }, applySorter: function (sorter) { var me = this, cfg = sorter; if (cfg && !cfg.isInstance) { if (typeof cfg === 'string') { cfg = { sorterFn: cfg }; } if (typeof cfg.sorterFn === 'string') { cfg = me.scopeReplacer(cfg, sorter, 'sorterFn', 'setSorterFn'); } sorter = new Ext.util.Sorter(cfg); } return sorter; }, updateSorter: function(sorter, oldSorter) { var store = this.getGrid().getStore(), sorters = store ? store.getSorters() : null, at; // If our previous sorter is in the store, replace it with the new one or // just remove it if we don't have one. if (sorters) { if (oldSorter && (at = sorters.indexOf(oldSorter)) > -1) { if (sorter) { sorters.splice(at, 1, sorter); } else { sorters.remove(oldSorter); } } } }, applyHideShowMenuItem: function(hideShowMenuItem, oldHideShowMenuItem) { return Ext.Factory.widget.update(oldHideShowMenuItem, hideShowMenuItem, this, 'createHideShowMenuItem'); }, createHideShowMenuItem: function(defaults) { return Ext.apply({ text: this.getText(), checked: !this.getHidden(), column: this }, defaults); }, doDestroy: function () { var me = this, editor = me.getConfig('editor', false, true); me.destroyMembers('resizeListener', 'menu', 'hideShowMenuItem'); me.setScratchCell(null); if (editor && editor.isWidget) { editor.ownerCmp = null; Ext.destroy(editor); } this.mixins.toolable.doDestroy.call(this); me.callParent(); }, getInnerHtmlElement: function () { return this.textElement; }, /** * Returns the parameter to sort upon when sorting this header. By default this returns the dataIndex and will not * need to be overridden in most cases. * @return {String} */ getSortParam: function () { return this.getDataIndex(); }, applyCell: function(cell, oldCell) { // Allow the cell config object to be reconfigured. if (oldCell) { cell = Ext.apply(oldCell, cell); } return cell; }, createCell: function (row) { var me = this, cfg = { row: row, ownerCmp: row || me, column: me, width: me.rendered ? (me.getComputedWidth() || me.measureWidth()) : me.getWidth(), minWidth: me.getMinWidth() }, align = me.getAlign(), cellCfg; if (row && row.isSummaryRow) { cellCfg = me.getSummaryCell(); if (!cellCfg) { cellCfg = me.getCell(); if (cellCfg.xtype === 'widgetcell') { // We don't default to creating a widgetcell in a summary row, so // fallback to a normal cell cellCfg = Ext.apply({}, cellCfg); cellCfg.xtype = 'gridcell'; delete cellCfg.widget; } } } else { cellCfg = me.getCell(); } if (align) { // only put align on the config object if it is not null. This prevents // the column's default value of null from overriding a value set on the // cell's class definition (e.g. widgetcell) cfg.align = align; } if (row) { cfg.hidden = me.isHidden(row.getGrid().getHeaderContainer()); cfg.record = row.getRecord(); if (!(cfg.ui = row.getDefaultCellUI())) { delete cfg.ui; } } if (typeof cellCfg === 'string') { cfg.xtype = cellCfg; } else { Ext.apply(cfg, cellCfg); } return cfg; }, applyScratchCell: function(cell, oldCell) { var me = this; if (cell) { cell = Ext.create(me.createCell()); if (!cell.printValue) { // If this cell type (widgetcell) cannot print its value, fallback to // default gridcell Ext.destroy(cell); cell = me.createCell(); cell.xtype = 'gridcell'; cell = Ext.create(cell); } } if (oldCell) { oldCell.destroy(); } return cell; }, printValue: function (value) { var me = this, rows = me.getGrid().dataItems, cell; if (rows.length) { cell = rows[0].getCellByColumn(me); } cell = (cell && cell.printValue) ? cell : me.getScratchCell(); return cell.printValue(value); }, privates: { applySummary: function (summary) { if (summary) { summary = Ext.Factory.dataSummary(summary); } return summary; }, beginRefresh: function (context) { // This is called by our detached cells var me = this, grid = me.getGrid(); context = context || {}; context.column = me; context.grid = grid; // record = null // row = null context.store = grid.store; return context; }, sort: function (direction) { var me = this, sorter = me.getSorter(), grid = me.getGrid(), store = grid.getStore(), isSorted = sorter && store.getSorters().contains(sorter); // This is the "group by" column - we have to set the grouper and tellit to recacculate. // AbstractStore#group just calls its Collection's updateGrouper if passed a Grouper // because *something* in the grouper might have changed, but the config system would // reject that as not a change. if (store.isGrouped() && store.getGroupField() === me.getDataIndex()) { sorter = store.getGrouper(); me.setSorter(sorter); if (sorter.getDirection() !== direction) { sorter.toggle(); store.group(sorter); } return; } if (sorter) { // Our sorter is in the requested direction if (sorter.getDirection() === direction) { // If it is applied, we've nothing to do if (isSorted) { return; } } else { me.oldDirection = sorter.getDirection(); sorter.toggle(); } } else { me.setSorter({ property: me.getSortParam(), direction: direction }); sorter = me.getSorter(); } // If the sorter is already applied, just command the store to sort with no params. // If the grid is NOT configured with multi column sorting, then specify "replace". // Only if we are doing multi column sorting do we insert it as one of a multi set. store.sort.apply(store, isSorted ? [] : [sorter, grid.getMultiColumnSort() ? 'multi' : 'replace']); }, toggleSortState: function () { if (this.isSortable()) { this.sort(); } }, /** * Sets the column sort state according to the direction of the Sorter passed, or the direction String passed. * @param {Ext.util.Sorter/String} sorter A Sorter, or `'ASC'` or `'DESC'` */ setSortState: function (sorter) { // Set the UI state to reflect the state of any passed Sorter // Called by the grid's HeaderContainer on view refresh var me = this, direction, sortedCls = me.sortedCls, ascCls = sortedCls + '-asc', descCls = sortedCls + '-desc', ariaDom = me.ariaEl.dom, changed; if (sorter) { direction = sorter.isSorter ? sorter.getDirection() : sorter; } switch (direction) { case 'DESC': if (!me.hasCls(descCls)) { me.addCls([sortedCls, descCls]); me.sortState = 'DESC'; changed = true; } me.removeCls(ascCls); break; case 'ASC': if (!me.hasCls(ascCls)) { me.addCls([sortedCls, ascCls]); me.sortState = 'ASC'; changed = true; } me.removeCls(descCls); break; default: me.removeCls([sortedCls, ascCls, descCls]); me.sortState = null; break; } if (ariaDom) { if (me.sortState) { ariaDom.setAttribute('aria-sort', me.ariaSortStates[me.sortState]); } else { ariaDom.removeAttribute('aria-sort'); } } // we only want to fire the event if we have actually sorted if (changed) { me.fireEvent('sort', this, direction, me.oldDirection); } }, getVisibleCount: function () { var columns = this.getInnerItems(), len = columns.length, count = 0, i; for (i = 0; i < len; ++i) { if (columns[i].isHeaderGroup) { count += columns[i].getVisibleCount(); } else { count += columns[i].isHidden() ? 0 : 1; } } return count; }, onShow: function () { var toShow; // No visible subcolumns, then show the first child. if (!this.getVisibleCount()) { toShow = this.getComponent(0); if (toShow) { toShow.show(); } } }, doVisibilityCheck: function () { var me = this, columns = me.getInnerItems(), ln = columns.length, i, column; for (i = 0; i < ln; i++) { column = columns[i]; if (!column.isHidden()) { if (me.isHidden()) { if (me.initialized) { me.show(); } else { me.setHidden(false); } } return; } } me.hide(); }, onColumnShow: function (column) { if (this.getVisibleCount() > 0) { this.show(); } }, onColumnHide: function (column) { if (this.getVisibleCount() === 0) { this.hide(); } }, scopeReplacer: function (config, original, prop, setter) { var me = this, name = config[prop]; if (typeof name === 'string') { prop = prop || 'sorterFn'; setter = setter || 'setSorterFn'; if (original === config) { config = Ext.apply({}, config); } // The goal of this method is to be called only on the first use // and then replace itself (using the setter) to direct all future // calls to the proper method. config[prop] = function () { // NOTE "this" is Sorter or Grouper! var scope = me.resolveListenerScope(), fn = scope && scope[name], ret = 0; if (fn) { this[setter](fn.bind(scope)); ret = fn.apply(scope, arguments); } //<debug> else if (!scope) { Ext.raise('Cannot resolve scope for column ' + me.id); } else { Ext.raise('No such method "' + name + '" on ' + scope.$className); } //</debug> return ret; }; } return config; } } // privates /** * @method getEditor * Returns the value of {@link #editor} * * **Note:** This method will only have an implementation if the * {@link Ext.grid.plugin.Editable Editing plugin} has been enabled on the grid. * * @return {Mixed} The editor value. */ /** * @method setEditor * @chainable * Sets the form field to be used for editing. * * **Note:** This method will only have an implementation if the * {@link Ext.grid.plugin.Editable Editing plugin} has been enabled on the grid. * * @param {Object} field An object representing a field to be created. You must * include the column's dataIndex as the value of the field's name property when * setting the editor field. * * column.setEditor({ * xtype: 'textfield', * name: column.getDataIndex() * }); * * @return {Ext.column.Column} this */ /** * @method getDefaultEditor * Returns the value of {@link #defaultEditor} * * **Note:** This method will only have an implementation if the * {@link Ext.grid.plugin.Editable Editing plugin} has been enabled on the grid. * * @return {Mixed} The defaultEditor value. */ /** * @method setDefaultEditor * @chainable * Sets the default form field to be used for editing. * * **Note:** This method will only have an implementation if the * {@link Ext.grid.plugin.Editable Editing plugin} has been enabled on the grid. * * @param {Object} field An object representing a field to be created. You must * include the column's dataIndex as the value of the field's name property when * setting the default editor field. * * column.setDefaultEditor({ * xtype: 'textfield', * name: column.getDataIndex() * }); * * @return {Ext.column.Column} this */});