/**
* This class specifies the definition for a column inside a {@link Ext.grid.Panel}. It encompasses
* both the grid header configuration as well as displaying data within the grid itself. If the
* {@link #columns} configuration is specified, this column will become a column group and can
* contain other columns inside. In general, this class will not be created directly, rather
* 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"}
* ]
* });
*
* Ext.create('Ext.grid.Panel', {
* 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,
* forceFit: true,
* renderTo: Ext.getBody()
* });
*
* # Convenience Subclasses
*
* There are several column subclasses that provide default rendering for various data types
*
* - {@link Ext.grid.column.Action}: Renders icons that can respond to click events inline
* - {@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
* - {@link Ext.grid.column.Template}: Renders a value using an {@link Ext.XTemplate} using the record data
*
* # Setting Widths
*
* The columns are laid out by a {@link Ext.layout.container.HBox} layout, so a column can either
* be given an explicit width value or a {@link #flex} configuration. If no width is specified the grid will
* automatically the size the column to 100px.
*
* Group columns (columns with {@link #columns child columns}) may be sized using {@link #flex},
* in which case they will apply `forceFit` to their child columns so as not to leave blank space.
*
* If a group column is not flexed, its width is calculated by measuring the width of the
* child columns, so a width option should not be specified in that case.
*
* # 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
* - {@link #hideable}: Specifies whether the column can be hidden using the column menu
* - {@link #menuDisabled}: Disables the column header menu
* - {@link #cfg-draggable}: Specifies whether the column header can be reordered by dragging
* - {@link #groupable}: Specifies whether the grid can be grouped by the column dataIndex. See also {@link Ext.grid.feature.Grouping}
*
* # 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 Ext.grid.column.Column#renderer}: Allows the underlying store
* value to be transformed before being displayed in the grid
*
* ## State saving
*
* When the owning {@link Ext.grid.Panel Grid} is configured
* {@link Ext.grid.Panel#cfg-stateful}, it will save its column state (order and width)
* encapsulated within the default Panel state of changed width and height and
* collapsed/expanded state.
*
* On a `stateful` grid, not only should the Grid have a
* {@link Ext.grid.Panel#cfg-stateId}, each column of the grid should also be configured
* with a {@link #stateId} which identifies that column locally within the grid.
*
* Omitting the `stateId` config from the columns results in columns with generated
* internal ID's. The generated ID's are subject to change on each page load
* making it impossible for the state manager to restore the previous state of the
* columns.
*/
Ext.define('Ext.grid.column.Column', {
extend: 'Ext.grid.header.Container',
xtype: 'gridcolumn',
requires: [
'Ext.grid.ColumnComponentLayout',
'Ext.grid.ColumnLayout',
'Ext.app.bind.Parser' // for "format" support
],
alternateClassName: 'Ext.grid.Column',
config: {
triggerVisible: false,
/**
* @cfg {Function/String/Object/Ext.util.Sorter} sorter
* A Sorter, or sorter config object to apply when the standard user interface
* sort gesture 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.
*
* In 6.2.0, a `{@link Ext.app.ViewController controller}` method can be used
* like so:
*
* sorter: {
* sorterFn: 'sorterMethodName'
* }
*
* @since 6.0.1
*/
sorter: null,
/**
* @cfg {'start'/'center'/'end'} [align='start']
* Sets the alignment of the header and rendered columns.
* Possible values are: `'start'`, `'center'`, and `'end'`.
*
* Since 6.2.0, `'left'` and `'right'` will still work, but retain their meaning
* even when the application is in RTL mode.
*
* `'start'` and `'end'` always conform to the locale's text direction.
*/
align: 'start'
},
baseCls: Ext.baseCSSPrefix + 'column-header',
// Not the standard, automatically applied overCls because we must filter out overs of child headers.
hoverCls: Ext.baseCSSPrefix + 'column-header-over',
ariaRole: 'columnheader',
enableFocusableContainer: false,
sortState: null,
possibleSortStates: ['ASC', 'DESC'],
// These are not readable descriptions; the values go in the aria-sort attribute.
ariaSortStates: {
ASC: 'ascending',
DESC: 'descending'
},
childEls: [
'titleEl', 'triggerEl', 'textEl', 'textContainerEl', 'textInnerEl'
],
/**
* @private
* @cfg {Boolean} [headerWrap=false]
* The default setting indicates that external CSS rules dictate that the title is `white-space: nowrap` and
* therefore, width cannot affect the measured height by causing text wrapping. This is what the Sencha-supplied
* styles set. If you change those styles to allow text wrapping, you must set this to `true`.
*/
headerWrap: false,
renderTpl: [
'<div id="{id}-titleEl" data-ref="titleEl" role="presentation"',
'{tipMarkup}class="', Ext.baseCSSPrefix, 'column-header-inner<tpl if="!$comp.isContainer"> ', Ext.baseCSSPrefix, 'leaf-column-header</tpl>',
'<tpl if="empty"> ', Ext.baseCSSPrefix, 'column-header-inner-empty</tpl>">',
//
// TODO:
// When IE8 retires, revisit https://jsbin.com/honawo/quiet for better way to center header text
//
'<div id="{id}-textContainerEl" data-ref="textContainerEl" role="presentation" class="', Ext.baseCSSPrefix, 'column-header-text-container">',
'<div role="presentation" class="', Ext.baseCSSPrefix, 'column-header-text-wrapper">',
'<div id="{id}-textEl" data-ref="textEl" role="presentation" class="', Ext.baseCSSPrefix, 'column-header-text',
'{childElCls}">',
'<span id="{id}-textInnerEl" data-ref="textInnerEl" role="presentation" class="', Ext.baseCSSPrefix, 'column-header-text-inner">{text}</span>',
'</div>',
'{%',
'values.$comp.afterText(out, values);',
'%}',
'</div>',
'</div>',
'<tpl if="!menuDisabled">',
'<div id="{id}-triggerEl" data-ref="triggerEl" role="presentation" unselectable="on" class="', Ext.baseCSSPrefix, 'column-header-trigger',
'{childElCls}" style="{triggerStyle}"></div>',
'</tpl>',
'</div>',
'{%this.renderContainer(out,values)%}'
],
/**
* @cfg {Object[]} columns
* An optional array of sub-column definitions. This column becomes a group, and houses the columns defined in the
* `columns` config.
*
* Group columns may not be sortable. But they may be hideable and moveable. And you may move headers into and out
* of a group. Note that if all sub columns are dragged out of a group, the group is destroyed.
*/
/**
* @cfg {String} stateId
* An identifier which identifies this column uniquely within the owning grid's {@link #stateful state}.
*
* This does not have to be *globally* unique. A column's state is not saved standalone. It is encapsulated within
* the owning grid's state.
*/
/**
* @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 {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: '\u00a0',
/**
* @cfg {String} header
* The header text.
* @deprecated 4.0 Use {@link #text} instead.
*/
/**
* @cfg {String} menuText
* The text to render in the column visibility selection menu for this column. If not
* specified, will default to the text value.
*/
menuText: null,
/**
* @cfg {String} [emptyCellText=undefined]
* The text to display in empty cells (cells with a value of `undefined`, `null`, or `''`).
*
* Defaults to ` ` aka ` `.
*/
emptyCellText: '\u00a0',
/**
* @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} [enableTextSelection=false]
* True to enable text selection inside grid cells in this column.
*/
/**
* @cfg {Boolean} lockable
* If the grid is configured with {@link Ext.panel.Table#enableLocking enableLocking}, or has columns which are
* configured with a {@link #locked} value, this option may be used to disable user-driven locking or unlocking
* of this column. This column will remain in the side into which its own {@link #locked} configuration placed it.
*/
/**
* @cfg {Boolean} groupable
* If the grid uses a {@link Ext.grid.feature.Grouping}, this option may be used to disable the header menu
* item to group by the column selected. By default, the header menu group option is enabled. Set to false to
* disable (but still show) the group option in the header menu for the column.
*/
/**
* @cfg {Boolean} fixed
* True to prevent the column from being resizable.
* @deprecated 4.0 Use {@link #resizable} instead.
*/
/**
* @cfg {Boolean} [locked=false]
* True to lock this column in place. Implicitly enables locking on the grid.
* See also {@link Ext.grid.Panel#enableLocking}.
*/
/**
* @cfg {Boolean} [cellWrap=false]
* True to allow whitespace in this column's cells to wrap, and cause taller column height where
* necessary.
*
* This implicitly sets the {@link #variableRowHeight} config to `true`
*/
/**
* @cfg {Boolean} [variableRowHeight=false]
* True to indicate that data in this column may take on an unpredictable height, possibly differing from row to row.
*
* If this is set, then View refreshes, and removal and addition of new rows will result in an ExtJS layout of the grid
* in order to adjust for possible addition/removal of scrollbars in the case of data changing height.
*
* This config also tells the View's buffered renderer that row heights are unpredictable, and must be remeasured as the view is refreshed.
*/
/**
* @cfg {Boolean} resizable
* False to prevent the column from being resizable.
*/
resizable: true,
/**
* @cfg {Boolean} hideable
* False to prevent the user from hiding this column.
*/
hideable: true,
/**
* @cfg {Boolean} menuDisabled
* True to disable the column header menu containing sort/hide options.
*/
menuDisabled: false,
/**
* @cfg {Function/String} renderer
* A renderer is an 'interceptor' method which can be used to transform data (value,
* appearance, etc.) before it is rendered. Example:
*
* **NOTE:** In previous releases, a string was treated as a method on
* `Ext.util.Format` but that is now handled by the {@link #formatter} config.
*
* @param {Object} value The data value for the current cell
*
* renderer: function(value){
* // evaluates `value` to append either `person' or `people`
* return Ext.util.Format.plural(value, 'person', 'people');
* }
*
* @param {Object} metaData A collection of metadata about the current cell; can be
* used or modified by the renderer. Recognized properties are: `tdCls`, `tdAttr`,
* and `tdStyle`.
*
* To add style attributes to the `<td>` element, you must use the `tdStyle`
* property. Using a style attribute in the `tdAttr` property will override the
* styles the column sets, such as the width which will break the rendering.
*
* You can see an example of using the metaData parameter below.
*
* Ext.create('Ext.data.Store', {
* storeId: 'simpsonsStore',
* fields: ['class', 'attr', 'style'],
* data: {
* 'class': 'red-bg',
* 'attr': 'lightyellow',
* 'style': 'red'
* }
* });
*
* Ext.create('Ext.grid.Panel', {
* title: 'Simpsons',
* store: Ext.data.StoreManager.lookup('simpsonsStore'),
* columns: [{
* text: 'Name',
* dataIndex: 'class',
* renderer: function (value, metaData) {
* metaData.tdCls = value;
* return value;
* }
* }, {
* text: 'Email',
* dataIndex: 'attr',
* flex: 1,
* renderer: function (value, metaData) {
* metaData.tdAttr = 'bgcolor="' + value + '"';
* return value;
* }
* }, {
* text: 'Phone',
* dataIndex: 'style',
* renderer: function (value, metaData) {
* metaData.tdStyle = 'color:' + value;
* return value;
* }
* }],
* height: 200,
* width: 400,
* renderTo: Ext.getBody()
* });
*
* @param {Ext.data.Model} record The record for the current row
*
* renderer: function (value, metaData, record) {
* // evaluate the record's `updated` field and if truthy return the value
* // from the `newVal` field, else return value
* var updated = record.get('updated');
* return updated ? record.get('newVal') : value;
* }
*
* @param {Number} rowIndex The index of the current row
*
* renderer: function (value, metaData, record, rowIndex) {
* // style the cell differently for even / odd values
* var odd = (rowIndex % 2 === 0);
* metaData.tdStyle = 'color:' + (odd ? 'gray' : 'red');
* }
*
* @param {Number} colIndex The index of the current column
*
* var myRenderer = function(value, metaData, record, rowIndex, colIndex) {
* if (colIndex === 0) {
* metaData.tdAttr = 'data-qtip=' + value;
* }
* // additional logic to apply to values in all columns
* return value;
* }
*
* // using the same renderer on all columns you can process the value for
* // each column with the same logic and only set a tooltip on the first column
* renderer: myRenderer
*
* _See also {@link Ext.tip.QuickTipManager}_
*
* @param {Ext.data.Store} store The data store
*
* renderer: function (value, metaData, record, rowIndex, colIndex, store) {
* // style the cell differently depending on how the value relates to the
* // average of all values
* var average = store.average('grades');
* metaData.tdCls = (value < average) ? 'needsImprovement' : 'satisfactory';
* return value;
* }
*
* @param {Ext.view.View} view The data view
*
* renderer: function (value, metaData, record, rowIndex, colIndex, store, view) {
* // style the cell using the dataIndex of the column
* var headerCt = this.getHeaderContainer(),
* column = headerCt.getHeaderAtIndex(colIndex);
*
* metaData.tdCls = 'app-' + column.dataIndex;
* return value;
* }
*
* @return {String}
* The HTML string to be rendered.
* @controllable
*/
renderer: false,
/**
* @cfg {Function/String} updater
* An updater is a method which is used when records are updated, and an *existing* grid row needs updating.
* The method is passed the cell element and may manipulate it in any way.
*
* **Note**: The updater is required to insert the {@link #emptyCellText} if there
* is no value in the cell.
*
* Ext.create('Ext.grid.Panel', {
* title: 'Grades',
* store: {
* fields: ['originalScore', 'newScore'],
* data: [{
* originalScore: 70,
* newScore: 70
* }]
* },
* columns: [{
* text: 'Score',
* dataIndex: 'newScore',
* editor: 'numberfield',
* flex: 1,
* updater: function (cell, value, record, view) {
* var inner = Ext.get(cell).first(),
* originalScore = record.get('originalScore'),
* color = (value === originalScore) ? 'black' : (value > originalScore) ? 'green' : 'red';
*
* // set the color based on the current value relative to the originalScore value
* // * same = black
* // * higher = green
* // * less = red
* inner.applyStyles({
* color: color
* });
* // pass the value to the cell's inner el
* inner.setHtml(value);
* }
* }],
* height: 200,
* width: 400,
* renderTo: document.body,
* tbar: [{
* xtype: 'numberfield',
* fieldLabel: 'New Score',
* value: 70,
* listeners: {
* change: function (field, newValue) {
* this.up('grid').getStore().first().set('newScore', newValue);
* }
* }
* }]
* });
*
* If a string is passed it is assumed to be the name of a method defined by the
* {@link #method-getController ViewController} or an ancestor component configured as {@link #defaultListenerScope}.
* @cfg {HTMLElement} updater.cell The HTML cell element to update.
* @cfg {Object} updater.value The data value for the current cell
* @cfg {Ext.data.Model} updater.record The record for the current row
* @cfg {Ext.view.View} updater.view The current view
*
* **Note**: The updater is required to insert the {@link #emptyCellText} if there is no value in the cell.
*
* @controllable
*/
/**
* @cfg {Object} scope
* The scope to use when calling the
* {@link Ext.grid.column.Column#renderer} function.
*/
/**
* @method defaultRenderer
* When defined this will take precedence over the
* {@link Ext.grid.column.Column#renderer renderer} config.
* This is meant to be defined in subclasses that wish to supply their own renderer.
* @protected
* @template
*/
/**
* @cfg {Function/String} editRenderer
* A renderer to be used in conjunction with
* {@link Ext.grid.plugin.RowEditing RowEditing}. This renderer is used to display a
* custom value for non-editable fields.
*
* **Note:** The editRenderer is called when the roweditor is initially shown.
* Changes to the record during editing will not call editRenderer.
*
* var store = Ext.create('Ext.data.Store', {
* fields: ['name', 'email'],
* data: [{
* "name": "Finn",
* "email": "finn@adventuretime.com"
* }, {
* "name": "Jake",
* "email": "jake@adventuretime.com"
* }]
* });
*
* Ext.create('Ext.grid.Panel', {
* title: 'Land Of Ooo',
* store: store,
* columns: [{
* text: 'Name',
* dataIndex: 'name',
* editRenderer: function(value){
* return '<span style="color:gray;">' + value + '</span>';
* }
* }, {
* text: 'Email',
* dataIndex: 'email',
* flex: 1,
* editor: {
* xtype: 'textfield',
* allowBlank: false
* }
* }],
* plugins: {
* ptype: 'rowediting',
* clicksToEdit: 1
* },
* height: 200,
* width: 400,
* renderTo: document.body
* });
*
* @param {Object} value The data value for the current cell
*
* editRenderer: function(value){
* // evaluates `value` to append either `person' or `people`
* return Ext.util.Format.plural(value, 'person', 'people');
* }
*
* @param {Object} metaData **Note:** The metadata param is passed to the
* editRenderer, but is not used.
*
* @param {Ext.data.Model} record The record for the current row
*
* editRenderer: function (value, metaData, record) {
* // evaluate the record's `updated` field and if truthy return the value
* // from the `newVal` field, else return value
* var updated = record.get('updated');
* return updated ? record.get('newVal') : value;
* }
*
* @param {Number} rowIndex The index of the current row
*
* editRenderer: function (value, metaData, record, rowIndex) {
* // style the value differently for even / odd values
* var odd = (rowIndex % 2 === 0),
* color = (odd ? 'gray' : 'red');
* return '<span style="color:' + color + ';">' + value + '</span>';
* }
*
* @param {Number} colIndex The index of the current column
*
* @param {Ext.data.Store} store The data store
*
* editRenderer: function (value, metaData, record, rowIndex, colIndex, store) {
* // style the cell differently depending on how the value relates to the
* // average of all values
* var average = store.average('grades'),
* status = (value < average) ? 'needsImprovement' : 'satisfactory';
* return '<span class="' + status + '">' + value + '</span>';
* }
*
* @param {Ext.view.View} view The data view
*
* editRenderer: function (value, metaData, record, rowIndex, colIndex, store, view) {
* // style the value using the dataIndex of the column
* var headerCt = this.getHeaderContainer(),
* column = headerCt.getHeaderAtIndex(colIndex);
*
* return '<span class="app-' + column.dataIndex + '">' + value + '</span>';
* }
*
* @return {String}
* The HTML string to be rendered.
* @controllable
*/
/**
* @cfg {Function/String} summaryRenderer
* A renderer to be used in conjunction with the {@link Ext.grid.feature.Summary Summary} or
* {@link Ext.grid.feature.GroupingSummary GroupingSummary} features. This renderer is used to
* display a summary value for this column.
* @controllable
*/
/**
* @cfg {Boolean} draggable
* False to disable drag-drop reordering of this column.
*/
draggable: true,
/**
* @cfg {String} tooltip
* A tooltip to display for this column header
*/
/**
* @cfg {String} [tooltipType="qtip"]
* The type of {@link #tooltip} to use. Either 'qtip' for QuickTips or 'title' for title attribute.
*/
tooltipType: 'qtip',
// Header does not use the typical ComponentDraggable class and therefore we
// override this with an emptyFn. It is controlled at the HeaderDragZone.
initDraggable: Ext.emptyFn,
/**
* @cfg {String} tdCls
* A CSS class names to apply to the table cells for this column.
*/
tdCls: '',
/**
* @cfg {Object/String} editor
* An optional xtype or config object for a {@link Ext.form.field.Field Field} to use for editing.
* Only applicable if the grid is using an {@link Ext.grid.plugin.Editing Editing} plugin.
*
* **Note:** The {@link Ext.form.field.HtmlEditor HtmlEditor} field is not a
* supported editor field type.
*/
//<locale>
/**
* @cfg {String} [dirtyText="Cell value has been edited"]
* This text will be announced by Assistive Technologies such as screen readers when
* a cell with changed ("dirty") value is focused.
*/
dirtyText: "Cell value has been edited",
//</locale>
/**
* @cfg {Object/String} field
* Alias for {@link #editor}.
* @deprecated 4.0.5 Use {@link #editor} instead.
*/
/**
* @cfg {Boolean} producesHTML
* This flag indicates that the renderer produces HTML.
*
* If this column is going to be updated rapidly, and the
* {@link Ext.grid.column.Column#renderer} or {@link #cfg-updater} only produces
* text, then to avoid the expense of HTML parsing and element production during the
* update, this property may be configured as `false`.
*/
producesHTML: true,
/**
* @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. Widget, check and action columns are not relevant when data is
* exported. You can set this flag on any column that you want to be ignored during export.
*
* This is used by {@link Ext.grid.plugin.Clipboard clipboard plugin} and {@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}.
* 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',
* text: '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',
* text: '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',
* text: '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,
/**
* @property {Ext.dom.Element} triggerEl
* Element that acts as button for column header dropdown menu.
*/
/**
* @property {Ext.dom.Element} textEl
* Element that contains the text in column header.
*/
/**
* @cfg {Boolean} [cellFocusable=true]
* Configure as `false` to remove all cells in this column from navigation.
*
* This is currently used by the PivotGrid package to create columns which have
* no semantic role, but are purely for visual indentation purposes.
* @since 6.2.0.
*/
/**
* @property {Boolean} isHeader
* @deprecated see isColumn
* Set in this class to identify, at runtime, instances which are not instances of the
* HeaderContainer base class, but are in fact, the subclass: Header.
*/
isHeader: true,
/**
* @property {Boolean} isColumn
* @readonly
* Set in this class to identify, at runtime, instances which are not instances of the
* HeaderContainer base class, but are in fact simple column headers.
*/
isColumn: true,
scrollable: false, // Override scrollable config from HeaderContainr class
requiresMenu: false, // allow plugins to set this property to influence if menu can be disabled
tabIndex: -1,
ascSortCls: Ext.baseCSSPrefix + 'column-header-sort-ASC',
descSortCls: Ext.baseCSSPrefix + 'column-header-sort-DESC',
componentLayout: 'columncomponent',
groupSubHeaderCls: Ext.baseCSSPrefix + 'group-sub-header',
groupHeaderCls: Ext.baseCSSPrefix + 'group-header',
clickTargetName: 'titleEl',
// So that when removing from group headers which are then empty and then get destroyed, there's no child DOM left
detachOnRemove : true,
// We need to override the default component resizable behaviour here
initResizable: Ext.emptyFn,
// Property names to reference the different types of renderers and formatters that
// we can use.
rendererNames: {
column: 'renderer',
edit: 'editRenderer',
summary: 'summaryRenderer'
},
formatterNames: {
column: 'formatter',
edit: 'editFormatter',
summary: 'summaryFormatter'
},
initComponent: function() {
var me = this;
// Preserve the scope to resolve a custom renderer.
// Subclasses (TreeColumn) may insist on scope being this.
if (!me.rendererScope) {
me.rendererScope = me.scope;
}
if (me.header != null) {
me.text = me.header;
me.header = null;
}
if (me.cellWrap) {
me.tdCls = (me.tdCls || '') + ' ' + Ext.baseCSSPrefix + 'wrap-cell';
}
// A group header; It contains items which are themselves Headers
if (me.columns != null) {
me.isGroupHeader = true;
me.ariaRole = 'presentation';
//<debug>
if (me.dataIndex) {
Ext.raise('Ext.grid.column.Column: Group header may not accept a dataIndex');
}
if ((me.width && me.width !== Ext.grid.header.Container.prototype.defaultWidth)) {
Ext.raise('Ext.grid.column.Column: Group header does not support setting explicit widths. A group header either shrinkwraps its children, or must be flexed.');
}
//</debug>
// The headers become child items
me.items = me.columns;
me.columns = null;
me.cls = (me.cls||'') + ' ' + me.groupHeaderCls;
// A group cannot be sorted, or resized - it shrinkwraps its children
me.sortable = me.resizable = false;
me.align = 'center';
} else {
// Flexed Headers need to have a minWidth defined so that they can never be squeezed out of existence by the
// HeaderContainer's specialized Box layout, the ColumnLayout. The ColumnLayout's overridden calculateChildboxes
// method extends the available layout space to accommodate the "desiredWidth" of all the columns.
if (me.flex) {
me.minWidth = me.minWidth || Ext.grid.plugin.HeaderResizer.prototype.minColWidth;
}
}
me.addCls(Ext.baseCSSPrefix + 'column-header-align-' + me.align);
// Set up the renderer types: 'renderer', 'editRenderer', and 'summaryRenderer'
me.setupRenderer();
me.setupRenderer('edit');
me.setupRenderer('summary');
// Initialize as a HeaderContainer
me.callParent(arguments);
},
beforeLayout: function() {
var items = this.items,
len,
i,
hasFlexedChildren;
if (!Ext.isArray(items)) {
items = items.items;
}
len = items.length;
if (len) {
for (i = 0; !hasFlexedChildren && i < len; i++) {
hasFlexedChildren = items[i].flex;
}
// If all children have been given a width, we must fall back to shrinkwrapping them.
if (!hasFlexedChildren) {
this.flex = null;
}
}
this.callParent();
},
onAdded: function(container, pos, instanced) {
var me = this;
me.callParent([container, pos, instanced]);
if (!me.headerId) {
me.calculateHeaderId();
}
me.configureStateInfo();
},
_initSorterFn: function (a, b) {
// NOTE: this method is placed as a "sorterFn" on a Sorter instance,
// so "this" is not a Column! Our goal is to replace the sorterFn of
// this Sorter on first use and then never get called again.
var sorter = this,
column = sorter.column,
scope = column.resolveListenerScope(),
name = sorter.methodName,
fn = scope && scope[name],
ret = 0;
if (fn) {
sorter.setSorterFn(fn);
sorter.column = null; // no need anymore (GC friendly)
// We are called by sort() so the ASC/DESC will be applied to what
// we return. Therefore, the correct delegation is to directly call
// the real sorterFn directly.
ret = fn.call(scope, a, b);
}
//<debug>
else if (!scope) {
Ext.raise('Cannot resolve scope for column ' + column.id);
}
else {
Ext.raise('No such method "' + name + '" on ' + scope.$className);
}
//</debug>
return ret;
},
applySorter: function (sorter) {
var me = this,
sorterFn = sorter ? sorter.sorterFn : null,
ret;
if (typeof sorterFn === 'string') {
// Instead of treating a string as a fieldname, it makes more sense to
// expect it to be a sortFn on the controller.
ret = new Ext.util.Sorter(Ext.applyIf({
sorterFn: me._initSorterFn
}, sorter));
ret.methodName = sorterFn;
ret.column = me;
}
else {
// Have the sorter spec decoded by the collection that will host it.
ret = me.getRootHeaderCt().up('tablepanel').store.getData().
getSorters().decodeSorter(sorter);
}
return ret;
},
updateAlign: function(align) {
// Translate according to the locale.
// This property is read by Ext.view.Table#renderCell
this.textAlign = this._alignMap[align] || align;
},
bindFormatter: function (format) {
var me = this;
return function (v) {
return format(v, me.rendererScope || me.resolveListenerScope());
};
},
bindRenderer: function (renderer) {
var me = this;
//<debug>
if (renderer in Ext.util.Format) {
Ext.log.warn('Use "formatter" config instead of "renderer" to use ' +
'Ext.util.Format to format cell values');
}
//</debug>
me.hasCustomRenderer = true;
return function () {
return Ext.callback(renderer, me.rendererScope, arguments, 0, me);
};
},
setupRenderer: function (type) {
// type can be null or 'edit', or 'summary'
type = type || 'column';
var me = this,
format = me[me.formatterNames[type]],
renderer = me[me.rendererNames[type]],
isColumnRenderer = type === 'column',
parser, dynamic;
if (!format) {
if (renderer) {
// Resolve a string renderer into the correct property: 'renderer', 'editRenderer', or 'summaryRenderer'
if (typeof renderer === 'string') {
renderer = me[me.rendererNames[type]] = me.bindRenderer(renderer);
dynamic = true;
}
if (isColumnRenderer) {
// If we are setting up a normal column renderer, detect if it's a custom one (reads more than one parameter)
// We can't read the arg list until we resolve the scope, so we must assume
// it's a renderer that needs a full update if it's dynamic
me.hasCustomRenderer = dynamic || renderer.length > 1;
}
}
// Column renderer could not be resolved: use the default one.
else if (isColumnRenderer && me.defaultRenderer) {
me.renderer = me.defaultRenderer;
me.usingDefaultRenderer = true;
}
} else {
/**
* @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 5.0.0
*/
parser = Ext.app.bind.Parser.fly(format);
format = parser.compileFormat();
parser.release();
me[me.formatterNames[type]] = null; // processed - trees come back here to add its renderer
// Set up the correct property: 'renderer', 'editRenderer', or 'summaryRenderer'
me[me.rendererNames[type]] = me.bindFormatter(format);
}
},
getView: function() {
var rootHeaderCt = this.getRootHeaderCt();
if (rootHeaderCt) {
return rootHeaderCt.view;
}
},
onFocusLeave: function(e) {
this.callParent([e]);
if (this.activeMenu) {
this.activeMenu.hide();
}
},
initItems: function() {
var me = this;
me.callParent(arguments);
if (me.isGroupHeader) {
// We need to hide the groupheader straightaway if it's configured as hidden or all its children are.
if (me.config.hidden || !me.hasVisibleChildColumns()) {
me.hide();
}
}
},
hasVisibleChildColumns: function() {
var items = this.items.items,
len = items.length,
i, item;
for (i = 0; i < len; ++i) {
item = items[i];
if (item.isColumn && !item.hidden) {
return true;
}
}
return false;
},
onAdd: function (child) {
var me = this;
if (child.isColumn) {
child.isSubHeader = true;
child.addCls(me.groupSubHeaderCls);
}
if (me.isGroupHeader && me.hidden && me.hasVisibleChildColumns()) {
me.show();
}
me.callParent([child]);
},
onRemove: function(child, isDestroying) {
var me = this;
if (child.isSubHeader) {
child.isSubHeader = false;
child.removeCls(me.groupSubHeaderCls);
}
me.callParent([child, isDestroying]);
// By this point, the component will be removed from the items collection.
//
// Note that we don't want to remove any grouped headers that have a descendant that is currently the drag target of an even lower stacked
// grouped header. See the comments in Ext.grid.header.Container#isNested.
if (!(me.destroyed || me.destroying) && !me.hasVisibleChildColumns() &&
(me.ownerCt && !me.ownerCt.isNested())) {
me.hide();
}
},
initRenderData: function() {
var me = this,
tipMarkup = '',
tip = me.tooltip,
text = me.text,
attr = me.tooltipType === 'qtip' ? 'data-qtip' : 'title';
if (!Ext.isEmpty(tip)) {
tipMarkup = attr + '="' + tip + '" ';
}
return Ext.applyIf(me.callParent(arguments), {
text: text,
empty: me.isEmptyText(text),
menuDisabled: me.menuDisabled,
tipMarkup: tipMarkup,
triggerStyle: this.getTriggerVisible() ? 'display:block' : ''
});
},
applyColumnState: function (state, storeState) {
var me = this,
sorter = me.getSorter(),
stateSorters = storeState && storeState.sorters,
len, i, savedSorter, mySorterId;
// If we have been configured with a sorter, then there SHOULD be a sorter config
// in the storeState with a corresponding ID from which we must restore our sorter's state.
// (The only state we can restore is direction).
// Then we replace the state entry with the real sorter. We MUST do this because the sorter
// is likely to have a custom sortFn.
if (sorter && stateSorters && (len = stateSorters.length)) {
mySorterId = sorter.getId();
for (i = 0; !savedSorter && i < len; i++) {
if (stateSorters[i].id === mySorterId) {
sorter.setDirection(stateSorters[i].direction);
stateSorters[i] = sorter;
break;
}
}
}
// apply any columns
me.applyColumnsState(state.columns);
// Only state properties which were saved should be restored.
// (Only user-changed properties were saved by getState)
if (state.hidden != null) {
me.hidden = state.hidden;
}
if (state.locked != null) {
me.locked = state.locked;
}
if (state.sortable != null) {
me.sortable = state.sortable;
}
if (state.width != null) {
me.flex = null;
me.width = state.width;
} else if (state.flex != null) {
me.width = null;
me.flex = state.flex;
}
},
getColumnState: function () {
var me = this,
items = me.items.items,
state = {
id: me.getStateId()
};
me.savePropsToState(['hidden', 'sortable', 'locked', 'flex', 'width'], state);
// Check for the existence of items, since column.Action won't have them
if (me.isGroupHeader && items && items.length) {
state.columns = me.getColumnsState();
}
if ('width' in state) {
delete state.flex; // width wins
}
return state;
},
/**
* Sets the header text for this Column.
* @param {String} text The header to display on this Column.
*/
setText: function(text) {
var me = this,
grid;
me.text = text;
if (me.rendered) {
grid = me.getView().ownerGrid;
me.textInnerEl.setHtml(text);
me.titleEl.toggleCls(Ext.baseCSSPrefix + 'column-header-inner-empty', me.isEmptyText(text));
grid.syncHeaderVisibility();
}
},
/**
* Returns the index of this column only if this column is a base level Column. If it
* is a group column, it returns `false`.
* @return {Number}
*/
getIndex: function() {
return this.isGroupColumn ? false : this.getRootHeaderCt().getHeaderIndex(this);
},
/**
* 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.
return this.visibleIndex != null ? this.visibleIndex : this.isGroupColumn ? false : Ext.Array.indexOf(this.getRootHeaderCt().getVisibleGridColumns(), this);
},
getLabelChain: function() {
var child = this,
labels = [],
parent;
while ((parent = child.up('headercontainer'))) {
if (parent.text) {
labels.unshift(Ext.util.Format.stripTags(parent.text));
}
child = parent;
}
return labels;
},
beforeRender: function() {
var me = this,
rootHeaderCt = me.getRootHeaderCt(),
isSortable = me.isSortable(),
labels = [],
ariaAttr;
me.callParent();
// Disable the menu if there's nothing to show in the menu, ie:
// Column cannot be sorted, grouped or locked, and there are no grid columns which may be hidden
if (!me.requiresMenu && !isSortable && !me.groupable &&
!me.lockable && (rootHeaderCt.grid.enableColumnHide === false ||
!rootHeaderCt.getHideableColumns().length)) {
me.menuDisabled = true;
}
// Wrapping text may cause unpredictable line heights.
// variableRowHeight is interrogated by the View for all visible columns to determine whether
// addition of new rows should cause an ExtJS layout.
// The View's summation of the presence of visible variableRowHeight columns is also used by
// any buffered renderer to determine how row height should be calculated when determining scroll range.
if (me.cellWrap) {
me.variableRowHeight = true;
}
ariaAttr = me.ariaRenderAttributes || (me.ariaRenderAttributes = {});
// Ext JS does not support editable column headers
ariaAttr['aria-readonly'] = true;
if (isSortable) {
ariaAttr['aria-sort'] = me.ariaSortStates[me.sortState];
}
if (me.isSubHeader) {
labels = me.getLabelChain();
if (me.text) {
labels.push(Ext.util.Format.stripTags(me.text));
}
if (labels.length) {
ariaAttr['aria-label'] = labels.join(' ');
}
}
me.protoEl.unselectable();
},
getTriggerElWidth: function() {
var me = this,
triggerEl = me.triggerEl,
width = me.self.triggerElWidth;
if (triggerEl && width === undefined) {
triggerEl.setStyle('display', 'block');
width = me.self.triggerElWidth = triggerEl.getWidth();
triggerEl.setStyle('display', '');
}
return width;
},
afterComponentLayout: function(width, height, oldWidth, oldHeight) {
var me = this,
rootHeaderCt = me.getRootHeaderCt();
me.callParent(arguments);
if (rootHeaderCt && (oldWidth != null || me.flex) && width !== oldWidth) {
rootHeaderCt.onHeaderResize(me, width);
}
},
doDestroy: function() {
// force destroy on the textEl, IE reports a leak
Ext.destroy(this.field, this.editor);
this.callParent();
},
onTitleMouseOver: function() {
this.titleEl.addCls(this.hoverCls);
},
onTitleMouseOut: function() {
this.titleEl.removeCls(this.hoverCls);
},
onDownKey: function(e) {
if (this.triggerEl) {
this.onTitleElClick(e, this.triggerEl.dom || this.el.dom);
}
},
onEnterKey: function(e) {
this.onTitleElClick(e, this.el.dom);
},
/**
* @private
* Double click handler which, if on left or right edges, auto-sizes the column to the left.
* @param e The dblclick event
*/
onTitleElDblClick: function(e) {
var me = this,
prev,
leafColumns,
headerCt;
// On left edge, resize previous *leaf* column in the grid
if (me.isAtStartEdge(e)) {
// Look for the previous visible column header which is a leaf
// Note: previousNode can walk out of the container (this may be first child of a group)
prev = me.previousNode('gridcolumn:not([hidden]):not([isGroupHeader])');
// If found in the same grid, auto-size it
if (prev && prev.getRootHeaderCt() === me.getRootHeaderCt()) {
prev.autoSize();
}
}
// On right edge, resize this column, or last sub-column within it
else if (me.isAtEndEdge(e)) {
// Click on right but in child container - auto-size last leaf column
if (me.isGroupHeader && e.getPoint().isContainedBy(me.layout.innerCt)) {
leafColumns = me.query('gridcolumn:not([hidden]):not([isGroupHeader])');
me.getRootHeaderCt().autoSizeColumn(leafColumns[leafColumns.length - 1]);
return;
} else {
headerCt = me.getRootHeaderCt();
// Cannot resize the only column in a forceFit grid.
if (headerCt.visibleColumnManager.getColumns().length === 1 && headerCt.forceFit) {
return;
}
}
me.autoSize();
}
},
/**
* Sizes this Column to fit the max content width.
* *Note that group columns shrink-wrap around the size of leaf columns. Auto sizing
* a group column auto-sizes descendant leaf columns.*
*/
autoSize: function() {
var me = this,
leafColumns,
numLeaves, i,
headerCt;
// Group headers are shrinkwrap width, so auto-sizing one means auto-sizing leaf
// descendants.
if (me.isGroupHeader) {
leafColumns = me.query('gridcolumn:not([hidden]):not([isGroupHeader])');
numLeaves = leafColumns.length;
headerCt = me.getRootHeaderCt();
Ext.suspendLayouts();
for (i = 0; i < numLeaves; i++) {
headerCt.autoSizeColumn(leafColumns[i]);
}
Ext.resumeLayouts(true);
// If we are a isolated layout due to being one half of a locking asembly
// where one is collapsed, the top level Ext.grid.locking.Lockable#afterLayout
// will NOT have been called, so we have to explicitly run it here.
if (grid.ownerGrid.lockable && grid.isLayoutRoot()) {
grid.ownerGrid.syncLockableLayout();
}
return;
}
me.getRootHeaderCt().autoSizeColumn(me);
},
isEmptyText: function(text) {
return text == null || text === ' ' || text === ' ' || text === '';
},
onTitleElClick: function(e, t, sortOnClick) {
var me = this,
activeHeader, prevSibling, tapMargin;
// Tap on the resize zone triggers the menu
if (e.pointerType === 'touch') {
prevSibling = me.previousSibling(':not([hidden])');
// Tap on right edge, activate this header
if (!me.menuDisabled) {
tapMargin = parseInt(me.triggerEl.getStyle('width'), 10);
// triggerEl can have width: auto, in which case we use handle width * 3
// that yields 30px for touch events. Should be enough in most cases.
if (isNaN(tapMargin)) {
tapMargin = me.getHandleWidth(e) * 3;
}
if (me.isAtEndEdge(e, tapMargin)) {
activeHeader = me;
}
}
// Tap on left edge, activate previous header
if (!activeHeader && prevSibling && !prevSibling.menuDisabled && me.isAtStartEdge(e)) {
activeHeader = prevSibling;
}
}
else {
// Firefox doesn't check the current target in a within check.
// Therefore we check the target directly and then within (ancestors)
activeHeader = me.triggerEl && (e.target === me.triggerEl.dom || t === me.triggerEl || e.within(me.triggerEl)) ? me : null;
}
// If it's not a click on the trigger or extreme edges. Or if we are called from a key handler, sort this column.
if (sortOnClick !== false && (!activeHeader && !me.isAtStartEdge(e) && !me.isAtEndEdge(e) || e.getKey())) {
me.toggleSortState();
}
return activeHeader;
},
/**
* @private
* Process UI events from the view. The owning TablePanel calls this method, relaying events from the TableView
* @param {String} type Event type, eg 'click'
* @param {Ext.view.Table} view TableView Component
* @param {HTMLElement} cell Cell HTMLElement the event took place within
* @param {Number} recordIndex Index of the associated Store Model (-1 if none)
* @param {Number} cellIndex Cell index within the row
* @param {Ext.event.Event} e Original event
*/
processEvent: function(type, view, cell, recordIndex, cellIndex, e) {
return this.fireEvent.apply(this, arguments);
},
isSortable: function() {
var rootHeader = this.getRootHeaderCt(),
grid = rootHeader ? rootHeader.grid : null,
sortable = this.sortable;
if (grid && grid.sortableColumns === false) {
sortable = false;
}
return sortable;
},
toggleSortState: function() {
if (this.isSortable()) {
this.sort();
}
},
sort: function(direction) {
var me = this,
grid = me.up('tablepanel'),
store = grid.store,
sorter = me.getSorter();
// Maintain backward compatibility.
// 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.
// Suspend layouts in case multiple views depend upon this grid's store (eg lockable assemblies)
Ext.suspendLayouts();
if (sorter) {
if (direction) {
sorter.setDirection(direction);
}
store.sort(sorter, grid.multiColumnSort ? 'multi' : 'replace');
} else {
store.sort(me.getSortParam(), direction, grid.multiColumnSort ? 'multi' : 'replace');
}
Ext.resumeLayouts(true);
// If we are a isolated layout due to being one half of a locking asembly
// where one is collapsed, the top level Ext.grid.locking.Lockable#afterLayout
// will NOT have been called, so we have to explicitly run it here.
if (grid.ownerGrid.lockable && grid.isLayoutRoot()) {
grid.ownerGrid.syncLockableLayout();
}
},
/**
* 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.dataIndex;
},
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 = sorter && sorter.getDirection(),
ascCls = me.ascSortCls,
descCls = me.descSortCls,
rootHeaderCt = me.getRootHeaderCt(),
ariaDom = me.ariaEl.dom,
changed;
switch (direction) {
case 'DESC':
if (!me.hasCls(descCls)) {
me.addCls(descCls);
me.sortState = 'DESC';
changed = true;
}
me.removeCls(ascCls);
break;
case 'ASC':
if (!me.hasCls(ascCls)) {
me.addCls(ascCls);
me.sortState = 'ASC';
changed = true;
}
me.removeCls(descCls);
break;
default:
me.removeCls([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) {
rootHeaderCt.fireEvent('sortchange', rootHeaderCt, me, direction);
}
},
/**
* Determines whether the UI should be allowed to offer an option to hide this column.
*
* A column may *not* be hidden if to do so would leave the grid with no visible columns.
*
* This is used to determine the enabled/disabled state of header hide menu items.
*/
isHideable: function() {
var result = {
hideCandidate: this,
result: this.hideable
};
if (result.result) {
this.ownerCt.bubble(this.hasOtherMenuEnabledChildren, null, [result]);
}
return result.result;
},
hasOtherMenuEnabledChildren: function(result) {
// Private bubble function used in determining whether this column is hideable.
// Executes in the scope of each component in the bubble sequence
var visibleChildren,
count;
// If we've bubbled out the top of the topmost HeaderContainer without finding a level with at least one visible,
// menu-enabled child *which is not the hideCandidate*, no hide!
if (!this.isXType('headercontainer')) {
result.result = false;
return false;
}
// If we find an ancestor level with at least one visible, menu-enabled child
// *which is not the hideCandidate*, then the hideCandidate is hideable.
// Note that we are not using CQ #id matchers - ':not(#' + result.hideCandidate.id + ')' - to exclude
// the hideCandidate because CQ queries are cached for the document's lifetime.
visibleChildren = this.query('>gridcolumn:not([hidden]):not([menuDisabled])');
count = visibleChildren.length;
if (Ext.Array.contains(visibleChildren, result.hideCandidate)) {
count--;
}
if (count) {
return false;
}
// If we go up, it's because the hideCandidate was the only hideable child, so *this* becomes the hide candidate.
result.hideCandidate = this;
},
/**
* Determines whether the UI should be allowed to offer an option to lock or unlock this column. Note
* that this includes dragging a column into the opposite side of a {@link Ext.panel.Table#enableLocking lockable} grid.
*
* A column may *not* be moved from one side to the other of a {@link Ext.panel.Table#enableLocking lockable} grid
* if to do so would leave one side with no visible columns.
*
* This is used to determine the enabled/disabled state of the lock/unlock
* menu item used in {@link Ext.panel.Table#enableLocking lockable} grids, and to determine droppabilty when dragging a header.
*/
isLockable: function() {
var result = {
result: this.lockable !== false
};
if (result.result) {
this.ownerCt.bubble(this.hasMultipleVisibleChildren, null, [result]);
}
return result.result;
},
/**
* Determines whether this column is in the locked side of a grid. It may be a descendant node of a locked column
* and as such will *not* have the {@link #locked} flag set.
*/
isLocked: function() {
return this.locked || !!this.up('[isColumn][locked]', '[isRootHeader]');
},
hasMultipleVisibleChildren: function(result) {
// Private bubble function used in determining whether this column is lockable.
// Executes in the scope of each component in the bubble sequence
// If we've bubbled out the top of the topmost HeaderContainer without finding a level with more than one visible child, no hide!
if (!this.isXType('headercontainer')) {
result.result = false;
return false;
}
// If we find an ancestor level with more than one visible child, it's fine to hide
if (this.query('>gridcolumn:not([hidden])').length > 1) {
return false;
}
},
hide: function () {
var me = this,
rootHeaderCt = me.getRootHeaderCt(),
owner = me.getRefOwner();
// During object construction, so just set the hidden flag and jump out
if (owner.constructing) {
me.callParent();
return me;
}
if (me.rendered && !me.isVisible()) {
// Already hidden
return me;
}
// Save our last shown width so we can gain space when shown back into fully flexed HeaderContainer.
// If we are, say, flex: 1 and all others are fixed width, then removing will do a layout which will
// convert all widths to flexes which will mean this flex value is too small.
if (rootHeaderCt.forceFit) {
me.visibleSiblingCount = rootHeaderCt.getVisibleGridColumns().length - 1;
if (me.flex) {
me.savedWidth = me.getWidth();
me.flex = null;
}
}
rootHeaderCt.beginChildHide();
Ext.suspendLayouts();
// owner is a group, hide call didn't come from the owner
if (owner.isGroupHeader) {
// The owner only has one item that isn't hidden and it's me; hide the owner.
if (me.isNestedGroupHeader()) {
owner.hide();
}
if (me.isSubHeader && !me.isGroupHeader && owner.query('>gridcolumn:not([hidden])').length === 1) {
owner.lastHiddenHeader = me;
}
}
me.callParent();
// Notify owning HeaderContainer. Will trigger a layout and a view refresh.
rootHeaderCt.endChildHide();
rootHeaderCt.onHeaderHide(me);
Ext.resumeLayouts(true);
// If we are a isolated layout due to being one half of a locking asembly
// where one is collapsed, the top level Ext.grid.locking.Lockable#afterLayout
// will NOT have been called, so we have to explicitly run it here.
if (rootHeaderCt.grid.ownerGrid.lockable && rootHeaderCt.grid.isLayoutRoot()) {
rootHeaderCt.grid.ownerGrid.syncLockableLayout();
}
return me;
},
show: function () {
var me = this,
rootHeaderCt = me.getRootHeaderCt(),
ownerCt = me.getRefOwner();
if (me.isVisible()) {
return me;
}
if (ownerCt.isGroupHeader) {
ownerCt.lastHiddenHeader = null;
}
if (me.rendered) {
// Size all other columns to accommodate re-shown column.
if (rootHeaderCt.forceFit) {
rootHeaderCt.applyForceFit(me);
}
}
Ext.suspendLayouts();
// If a sub header, ensure that the group header is visible
if (me.isSubHeader && ownerCt.hidden) {
ownerCt.show(false, true);
}
me.callParent(arguments);
if (me.isGroupHeader) {
me.maybeShowNestedGroupHeader();
}
// Notify owning HeaderContainer. Will trigger a layout and a view refresh.
ownerCt = me.getRootHeaderCt();
if (ownerCt) {
ownerCt.onHeaderShow(me);
}
Ext.resumeLayouts(true);
// If we are a isolated layout due to being one half of a locking asembly
// where one is collapsed, the top level Ext.grid.locking.Lockable#afterLayout
// will NOT have been called, so we have to explicitly run it here.
if (rootHeaderCt.grid.ownerGrid.lockable && rootHeaderCt.grid.isLayoutRoot()) {
rootHeaderCt.grid.ownerGrid.syncLockableLayout();
}
return me;
},
/**
* @private
* Decides whether the column needs updating
* @return {Number} 0 = Doesn't need update.
* 1 = Column needs update, and renderer has > 1 argument; We need to render a whole new HTML item.
* 2 = Column needs update, but renderer has 1 argument or column uses an updater.
*/
shouldUpdateCell: function(record, changedFieldNames) {
// If the column has a renderer which peeks and pokes at other data,
// return 1 which means that a whole new TableView item must be rendered.
//
// Note that widget columns shouldn't ever be updated.
if (!this.preventUpdate) {
if (this.hasCustomRenderer) {
return 1;
}
// If there is a changed field list, and it's NOT a custom column renderer
// (meaning it doesn't peek at other data, but just uses the raw field value),
// we only have to update it if the column's field is among those changes.
if (changedFieldNames) {
var len = changedFieldNames.length,
i, field;
for (i = 0; i < len; ++i) {
field = changedFieldNames[i];
if (field === this.dataIndex || field === record.idProperty) {
return 2;
}
}
} else {
return 2;
}
}
},
getCellWidth: function() {
var me = this,
result;
if (me.rendered && me.componentLayout && me.componentLayout.lastComponentSize) {
// headers always have either a width or a flex
// because HeaderContainer sets a defaults width
// therefore we can ignore the natural width
// we use the componentLayout's tracked width so that
// we can calculate the desired width when rendered
// but not visible because its being obscured by a layout
result = me.componentLayout.lastComponentSize.width;
} else if (me.width) {
result = me.width;
}
// This is a group header.
// Use getTableWidth and remember that getTableWidth adjusts for column lines and box model
else if (!me.isColumn) {
result = me.getTableWidth();
}
return result;
},
getCellId: function() {
return Ext.baseCSSPrefix + 'grid-cell-' + this.getItemId();
},
getCellSelector: function() {
var view = this.getView();
// We must explicitly access the view's cell selector as well as this column's own ID class because
// <col> elements are given this column's ID class.
// If we are still atached to a view. If not, the identifying class will do.
return (view ? view.getCellSelector() : '') + '.' + this.getCellId();
},
getCellInnerSelector: function() {
return this.getCellSelector() + ' .' + Ext.baseCSSPrefix + 'grid-cell-inner';
},
isAtStartEdge: function(e) {
var offset = e.getXY()[0] - this.getX();
// To the left of the first column, not over
if (offset < 0 && this.getIndex() === 0) {
return false;
}
return (offset < this.getHandleWidth(e));
},
isAtEndEdge: function(e, margin) {
return (this.getX() + this.getWidth() - e.getXY()[0] <= (margin || this.getHandleWidth(e)));
},
getHandleWidth: function(e) {
return e.pointerType === 'touch' ? 10 : 4;
},
setMenuActive: function(menu) {
// Called when the column menu is activated/deactivated.
// Change the UI to indicate active/inactive menu
this.activeMenu = menu;
this.titleEl[menu ? 'addCls' : 'removeCls'](this.headerOpenCls);
},
privates: {
/**
* @private
* Mapping for locale-neutral align setting.
* Overridden in Ext.rtl.grid.column.Column
*/
_alignMap: {
start: 'left',
end: 'right'
},
/**
* A method called by the render template to allow extra content after the header text.
* @private
*/
afterText: function(out, values) {
if (this.dirtyText) {
this.dirtyTextElementId = this.id + '-dirty-cell-text';
out.push(
'<span id="' + this.dirtyTextElementId + '" class="' +
Ext.baseCSSPrefix + 'hidden-offsets">' + this.dirtyText +
'</span>'
);
}
},
calculateHeaderId: function() {
var me = this,
ownerGrid,
counterOwner, items, item, i, len;
if (!me.headerId) {
// Sequential header counter MUST be based on the top level grid to avoid duplicates from sides
// of a lockable assembly.
ownerGrid = me.up('tablepanel');
if (!ownerGrid) {
return;
}
items = me.items.items;
// Action column has items as an array, so skip out here.
if (items) {
for (i = 0, len = items.length; i < len; ++i) {
item = items[i];
if (item.isColumn) {
item.calculateHeaderId();
}
}
}
counterOwner = ownerGrid ? ownerGrid.ownerGrid : me.getRootHeaderCt();
counterOwner.headerCounter = (counterOwner.headerCounter || 0) + 1;
me.headerId = 'h' + counterOwner.headerCounter;
}
me.configureStateInfo();
},
configureStateInfo: function () {
var me = this,
sorter;
// MUST stamp a stateId into this object; state application relies on reading the property, NOT using the getter!
// Only generate a stateId if it really needs one.
if (!me.stateId) {
// This was the headerId generated in 4.0, so to preserve saved state, we now
// assign a default stateId in that same manner. The stateId's of a column are
// not global at the stateProvider, but are local to the grid state data. The
// headerId should still follow our standard naming convention.
me.stateId = me.initialConfig.id || me.headerId;
}
sorter = me.getSorter();
if (!me.hasSetSorter && sorter && !sorter.initialConfig.id) {
if (me.dataIndex || me.stateId) {
sorter.setId((me.dataIndex || me.stateId) + '-sorter');
me.hasSetSorter = true;
}
}
}
},
deprecated: {
5: {
methods: {
bindRenderer: function (renderer) {
// This method restores the pre-5 meaning of "renderer" as a string:
// a method in Ext.util.Format. But at least we don't send all of
// the renderer arguments at the poor thing!
return function (value) {
return Ext.util.Format[renderer](value);
};
}
}
}
}
// intentionally omit getEditor and setEditor definitions bc we applyIf into columns
// when the editing plugin is injected
/**
* @method getEditor
* Retrieves the editing field for editing associated with this header. If the
* field has not been instantiated it will be created.
*
* **Note:** This method will only have an implementation if an Editing plugin has
* been enabled on the grid ({@link Ext.grid.plugin.CellEditing cellediting} /
* {@link Ext.grid.plugin.RowEditing rowediting}).
*
* @param {Object} [record] The {@link Ext.data.Model Model} instance being edited.
* @param {Object/String} [defaultField] An xtype or config object for a
* {@link Ext.form.field.Field Field} to be created as the default editor if it does
* not already exist
* @return {Ext.form.field.Field/Boolean} The editor field associated with
* this column. Returns false if there is no field associated with the
* {@link Ext.grid.column.Column Column}.
*/
/**
* @method setEditor
* Sets the form field to be used for editing.
*
* **Note:** This method will only have an implementation if an Editing plugin has
* been enabled on the grid ({@link Ext.grid.plugin.CellEditing cellediting} /
* {@link Ext.grid.plugin.RowEditing rowediting}).
*
* @param {Object} field An object representing a field to be created. If no xtype is specified a 'textfield' is
* assumed.
*/
});