/** * Grids are an excellent way of showing large amounts of tabular data on the client side. * Essentially a supercharged `<table>`, Grid makes it easy to fetch, sort and filter large * amounts of data. * * Grids are composed of two main pieces - a {@link Ext.data.Store Store} full of data and * a set of columns to render. * * ## A Basic Grid * * var store = Ext.create('Ext.data.Store', { * fields: ['name', 'email', 'phone'], * data: [ * { 'name': 'Lisa', "email":"[email protected]", "phone":"555-111-1224" }, * { 'name': 'Bart', "email":"[email protected]", "phone":"555-222-1234" }, * { 'name': 'Homer', "email":"[email protected]", "phone":"555-222-1244" }, * { 'name': 'Marge', "email":"[email protected]", "phone":"555-222-1254" } * ] * }); * * Ext.create('Ext.grid.Grid', { * title: 'Simpsons', * * store: store, * * columns: [ * { text: 'Name', dataIndex: 'name', width: 200 }, * { text: 'Email', dataIndex: 'email', width: 250 }, * { text: 'Phone', dataIndex: 'phone', width: 120 } * ], * * height: 200, * layout: 'fit', * fullscreen: true * }); * * The code above produces a simple grid with three columns. We specified a Store which will * load JSON data inline. In most apps we would be placing the grid inside another container * and wouldn't need to provide the {@link #height}, {@link #width} and * {@link #cfg-fullscreen} options but they are included here to for demonstration. * * The grid we created above will contain a header bar with a title ('Simpsons'), a row of * column headers directly underneath and finally the grid rows under the headers. * * ## Columns * * By default, each {@link Ext.grid.column.Column column} is sortable and toggles between * ascending and descending sorting when you click on its header. There are several basic * configs that can be applied to columns to change these behaviors. For example: * * columns: [ * { * text: 'Name', * dataIndex: 'name', * sortable: false, // column cannot be sorted * width: 250 * }, * { * text: 'Email', * dataIndex: 'email', * hidden: true // column is initially hidden * }, * { * text: 'Phone', * dataIndex: 'phone', * width: 100 * } * ] * * We turned off sorting on the 'Name' column so clicking its header now has no effect. We * also made the Email column hidden by default (it can be shown again by using the * {@link Ext.grid.plugin.ViewOptions ViewOptions} plugin). See the * {@link Ext.grid.column.Column column class} for more details. * * A top-level column definition may contain a `columns` configuration. This means that the * resulting header will be a group header, and will contain the child columns. * * ## Rows and Cells * * Grid extends the `{@link Ext.dataview.List List}` component and connects records in the * store to `{@link Ext.grid.Row row components}` for the list's items. The Row component * utilizes the configs of the grid's {@link Ext.grid.column.Column columns} to create the * appropriate type of {@link Ext.grid.cell.Base cells}. Essentially, a Row is a container * for {@link Ext.Widget Cell widgets}. * * For the most part, configuring a grid is about configuring the columns and their cells. * There are several built-in column types to display specific types of data: * * - {@link Ext.grid.column.Boolean} for true/false values. * - {@link Ext.grid.column.Date} for date/time values. * - {@link Ext.grid.column.Number} for numeric values. * * These columns specify (via their {@link Ext.grid.column.Column#cell cell config}) one * of these basic cell widget types: * * - {@link Ext.grid.cell.Boolean} * - {@link Ext.grid.cell.Date} * - {@link Ext.grid.cell.Number} * * In addition to the above basic cell types, there are two other useful cell types to * know about: * * - {@link Ext.grid.cell.Text} is the base class for the boolean, date and number cell * classes. It is useful when a cell contains only text. * - {@link Ext.grid.cell.Widget} is a cell class that manages a single child item (either * a {@link Ext.Component component} or a {@link Ext.Widget widget}). The child item is * configured using the `{@link Ext.grid.cell.Widget#widget widget config}`. The most * important part of this config is the `xtype` of the child item to create. * * ## Cells and Binding * * One technique to controll cell content and styling is to use data binding to target * cell configs like {@link Ext.grid.cell.Base#cls} and {@link Ext.grid.cell.Base#bodyCls}. * This is done by assigning a {@link Ext.app.ViewModel viewModel} to each Row like so: * * itemConfig: { * viewModel: true // create default ViewModel for each item (i.e., Row) * } * * Now that each Row has a ViewModel, cells can bind to the fields of the associated record * like so: * * columns: [{ * ... * cell: { * bind: { * cls: '{record.someCls}' * } * } * }] * * The "record" property in the ViewModel is managed by the Row. As Row instances are * recycled due to buffered rendering, the associated record instance simply changes over * time. * * ### Cell Widgets * * When using {@link Ext.grid.cell.Widget}, the contained widgets can also use binding to * configure themsleves using properties of the associated record. * * columns: [{ * ... * cell: { * xtype: 'widgetcell', * widget: { * xtype: 'button', * bind: { * text: 'Update {record.firstName}' * } * } * } * }] * * ### Row ViewModels * * In some cases a custom ViewModel could be useful, for example to provide useful values * via {@link Ext.app.ViewModel#formulas formulas}. * * itemConfig: { * viewModel: { * type: 'rowViewModel' * } * } * * ## Renderers and Templates * * Columns provide two other mechanisms to format their cell content: * * - {@link Ext.grid.column.Column#renderer} * - {@link Ext.grid.column.Column#tpl} * * These column configs are processed by the {@link Ext.grid.column.Cell default cell type} * for a column. These configs have some downsides compared to data binding but are provided * for compatibility with previous releases. * * - Renderers and templates must update the cell content when _any_ field changes. They * cannot assume that only changes to the dataIndex will affect the rendering. Using * data binding, only the configs affected by the changed data will be updated. * - Updates are processed synchronously in response to the record update notification. * Contrast to ViewModels which provide a buffered update mechanism. * - Constructing HTML blocks in code (even in a template) is a common cause of security * problems such as XSS attacks. * * ## Sorting & Filtering * * Every grid is attached to a {@link Ext.data.Store Store}, which provides multi-sort and * filtering capabilities. It's easy to set up a grid to be sorted from the start: * * var myGrid = Ext.create('Ext.grid.Panel', { * store: { * fields: ['name', 'email', 'phone'], * sorters: ['name', 'phone'] * }, * columns: [ * { text: 'Name', dataIndex: 'name' }, * { text: 'Email', dataIndex: 'email' } * ] * }); * * Sorting at run time is easily accomplished by simply clicking each column header. If you * need to perform sorting on more than one field at run time it's easy to do so by adding * new sorters to the store: * * myGrid.store.sort([ * { property: 'name', direction: 'ASC' }, * { property: 'email', direction: 'DESC' } * ]); * * See {@link Ext.data.Store} for examples of filtering. * * ## Plugins * * Grid supports addition of extra functionality through plugins: * * - {@link Ext.grid.plugin.ViewOptions ViewOptions} - adds the ability to show/hide * columns and reorder them. * * - {@link Ext.grid.plugin.ColumnResizing ColumnResizing} - allows for the ability to * resize columns. * * - {@link Ext.grid.plugin.Editable Editable} - editing grid contents one row at a time. * * - {@link Ext.grid.plugin.RowOperations RowOperations} - selecting and performing tasks * on severalrows at a time (e.g. deleting them). * * - {@link Ext.grid.plugin.PagingToolbar PagingToolbar} - adds a toolbar at the bottom of * the grid that allows you to quickly navigate to another page of data. * * - {@link Ext.grid.plugin.SummaryRow SummaryRow} - adds and pins an additional row to the * top of the grid that enables you to display summary data. */Ext.define('Ext.grid.Grid', { extend: 'Ext.dataview.List', xtype: 'grid', isGrid: true, requires: [ 'Ext.TitleBar', 'Ext.grid.NavigationModel', 'Ext.grid.Row', 'Ext.grid.column.Column', 'Ext.grid.column.Date', 'Ext.grid.column.Template', 'Ext.grid.menu.*', 'Ext.grid.HeaderContainer', 'Ext.grid.selection.*', 'Ext.grid.plugin.ColumnResizing', 'Ext.grid.plugin.HeaderReorder' ], mixins: [ 'Ext.mixin.ConfigProxy' ], storeEventListeners: { sort: 'onStoreSort' }, config: { /** * @cfg {Ext.grid.column.Column[]} columns (required) * An array of column definition objects which define all columns that appear in this grid. * Each column definition provides the header text for the column, and a definition of where * the data for that column comes from. * * This can also be a configuration object for a {Ext.grid.header.Container HeaderContainer} * which may override certain default configurations if necessary. For example, the special * layout may be overridden to use a simpler layout, or one can set default values shared * by all columns: * * columns: { * items: [ * { * text: "Column A" * dataIndex: "field_A", * width: 200 * },{ * text: "Column B", * dataIndex: "field_B", * width: 150 * }, * ... * ] * } * */ columns: null, /** * @cfg {Object} columnMenu * This is a config object which is used by columns in this grid to create their * header menus. * * The default column menu contains the following items. * * - A "Sort Ascending" menu item * - A "Sort Descending" menu item * - A Columns menu item with each of the columns in a sub-menu of check items * that is used to hide or show each column. * - A "Group by this field" menu item to enable grouping. * - A "Show in groups" check menu item to enable/disable grouping. * * These items have {@link #cfg!weight} of `-100`, `-90` and `-80` respectively to * place them at the start of the menu. * * This can be configured as `null` to prevent columns from showing a column menu. */ columnMenu: { cached: true, $value: { xtype: 'menu', weighted: true, align: 'tl-bl?', hideOnParentHide: false, // Persists when owning Column is hidden items: { sortAsc: { xtype: 'gridsortascmenuitem', group: 'sortDir', weight: -100 // Wants to be the first }, sortDesc: { xtype: 'gridsortdescmenuitem', group: 'sortDir', weight: -90 // Wants to be the second }, //--------------------------------- // Columns menu is inserted here //--------------------------------- groupByThis: { xtype: 'gridgroupbythismenuitem', handler: 'column.onGroupByThis', separator: true, weight: -50 }, showInGroups: { xtype: 'gridshowingroupsmenuitem', handler: 'column.onToggleShowInGroups', weight: -40 } } } }, /** * @cfg {Boolean} columnResize * Set to `false` to disable column resizing within this grid. */ columnResize: true, headerContainer: { xtype: 'headercontainer' }, /** * @cfg {Boolean} hideHeaders * `true` to hide the grid column headers. * * @since 6.0.1 */ hideHeaders: false, /** * @cfg {Boolean} enableColumnMove * Set to `false` to disable column reorder. * * **Note**: if `gridviewoptions` plugin is enabled on grids gets * precedence over `enableColumnMove` for touch supported device. */ enableColumnMove: true, /** * @cfg {Boolean} hideScrollbar * * @private */ hideScrollbar: null, /** * @hide * Grid Rows are not focusable. Cells are focusable. */ itemsFocusable: false, /** * @cfg {String} title * The title that will be displayed in the TitleBar at the top of this Grid. */ title: '', /** * @cfg {Object} titleBar * The TitleBar. */ titleBar: { xtype: 'titlebar', docked: 'top' }, /** * @cfg {Boolean} sortable * Configure as `false` to disable column sorting via clicking the header and via * the Sorting menu items. */ sortable: true, /** * @cfg {Boolean} multiColumnSort * Configure as `true` to have columns retain their sorted state after other * columns have been clicked upon to sort. * * As subsequent columns are clicked upon, they become the new primary sort key. * * Clicking on an already sorted column which is *not* the primary sort key does * not toggle its direction. Analogous to bringing a window to the top by * clicking it, this makes that column's field the primary sort key. Subsequent * clicks then toggle it. * * Clicking on a primary key column toggles `ASC` -> `DESC` -> no sorter. * * The column sorting menu items may be used to toggle the direction without * affecting the sorter priority. * * The maximum number of sorters allowed in a Store is configurable via its * underlying data collection. See {@link Ext.util.Collection#multiSortLimit} */ multiColumnSort: false, /** * @cfg {Ext.grid.menu.Columns} columnsMenuItem * The config object for the grid's column hide/show menu */ columnsMenuItem: { lazy: true, $value: { xtype: 'gridcolumnsmenu', weight: -80, separator: true } }, /** * @cfg {Boolean} [columnLines=false] * Configure as `true` to display lines between grid cells. */ columnLines: null, /** * @cfg {Boolean/Object} [rowNumbers=false] * Configure as `true` to a {@link Ext.grid.column.RowNumberer row numberer} * column which gravitates to the start of the grid. * * May be a {@link Ext.grid.column.RowNumberer} configuration object. For * instance to set the column title use: * * rowNumbers: { * text: 'Index' * } */ rowNumbers: null }, /** * @cfg {Object/Ext.grid.Row} itemConfig * The object is used to configure the {@link Ext.grid.Row rows) created by this Grid. * * An `xtype` property may be included to specify a user-supplied subclass of * {@link Ext.grid.Row}. * * See the {@link Ext.grid.row#cfg!body} and {@link Ext.grid.row#cfg!expandedField} * configs on the {@link Ext.grid.RowRow class} to easily add extra content to grid * rows. * * Be aware that if you specify a {@link Ext.grid.row#cfg!body row body}, you must * configure the owning grid with `{@link #variableHeights}: true`. */ itemConfig: { xtype: 'gridrow' }, /** * @cfg groupHeader * @inheritdoc */ groupHeader: { xtype: 'rowheader' }, /** * @cfg infinite * @inheritdoc */ infinite: true, // The type of navigationMode to create navigationModel: 'grid', /** * @cfg pinnedHeader * @inheritdoc */ pinnedHeader: { xtype: 'rowheader' }, /** * @cfg scrollable * @inheritdoc */ scrollable: true, /** * @cfg scrollToTopOnRefresh * @inheritdoc */ scrollToTopOnRefresh: false, /** * @cfg striped * @inheritdoc */ striped: true, // Our reserveScrollbar config is propagated down to the headerContainer proxyConfig: { headerContainer: [ /** * @cfg {Boolean} [reserveScrollbar=false] * *only meaningful on platforms which has space-consuming scroll bars* * * Configure as `true` to leave space for a scrollbar to appear even if the * content does not overflow. * * This is useful for trees which may expand and collapse causing visual * flickering when scrollbars appear or disappear. */ 'reserveScrollbar' ] }, /** * @cfg {Ext.grid.selection.Model} selectable * A configuration object which allows passing of configuration options to create or * reconfigure a {@link Ext.grid.selection.Model selection model}. * * The following options control what can be selected: * * - {@link Ext.grid.selection.Model#cfg!cells cells} * - {@link Ext.grid.selection.Model#cfg!columns columns} * - {@link Ext.grid.selection.Model#cfg!rows rows} * * These options control how selections can be made: * * - {@link Ext.grid.selection.Model#cfg!checkbox checkbox} * - {@link Ext.grid.selection.Model#cf!deselectable deselectable} * - {@link Ext.grid.selection.Model#cfg!drag drag} * - {@link Ext.grid.selection.Model#cfg!extensible extensible} * - {@link Ext.grid.selection.Model#cfg!mode mode} * - {@link Ext.grid.selection.Model#cfg!reducible reducible} */ /** * @event columnadd * Fires whenever a column is added to the Grid. * @param {Ext.grid.Grid} this The Grid instance. * @param {Ext.grid.column.Column} column The added column. * @param {Number} index The index of the added column. */ /** * @event columnmove * Fires whenever a column is moved in the grid. * @param {Ext.grid.Grid} this The Grid instance. * @param {Ext.grid.column.Column} column The moved column. * @param {Number} fromIndex The index the column was moved from. * @param {Number} toIndex The index the column was moved to. */ /** * @event columnremove * Fires whenever a column is removed from the Grid. * @param {Ext.grid.Grid} this The Grid instance. * @param {Ext.grid.column.Column} column The removed column. */ /** * @event columnshow * Fires whenever a column is shown in the Grid. * @param {Ext.grid.Grid} this The Grid instance. * @param {Ext.grid.column.Column} column The shown column. */ /** * @event columnhide * Fires whenever a column is hidden in the Grid. * @param {Ext.grid.Grid} this The Grid instance. * @param {Ext.grid.column.Column} column The shown column. */ /** * @event columnresize * Fires whenever a column is resized in the Grid. * @param {Ext.grid.Grid} this The Grid instance. * @param {Ext.grid.column.Column} column The resized column. * @param {Number} width The new column width. */ /** * @event columnsort * Fires whenever a column is sorted in the Grid. * @param {Ext.grid.Grid} this The Grid instance. * @param {Ext.grid.column.Column} column The sorted column. * @param {String} direction The direction of the sort on this Column. Either 'asc' * or 'desc'. */ /** * @event cellselection * Fires when cell selection is being used and cells are selected or deselected. * @param {Ext.grid.Grid} grid this Grid * @param {Ext.grid.selection.Rows} selection An object which encapsulates the * selected cell range(s). */ /** * @event columnselection * Fires when column selection is being used and columns are selected or deselected. * @param {Ext.grid.Grid} grid this Grid * @param {Ext.grid.selection.Columns} selection An object which encapsulates the * selected columns. */ /** * @event beforestartedit * Fires when editing is initiated, but before the value changes. Editing can be canceled by * returning false from the handler of this event. * @param {Ext.Editor} editor * @param {Ext.dom.Element} boundEl The underlying element bound to this editor * @param {Object} value The field value being set * @param {Ext.grid.Location} The location where actionable mode was successfully started * @since 7.0 */ /** * @event startedit * Fires when this editor is displayed * @param {Ext.Editor} editor * @param {Ext.dom.Element} boundEl The underlying element bound to this editor * @param {Object} value The starting field value * @param {Ext.grid.Location} The location where actionable mode was successfully started * @since 7.0 */ /** * @event beforecomplete * Fires after a change has been made to the field, but before the change is reflected in the * underlying field. Saving the change to the field can be canceled by returning false from * the handler of this event. Note that if the value has not changed and ignoreNoChange = true, * the editing will still end but this event will not fire since no edit actually occurred. * @param {Ext.Editor} editor * @param {Object} value The current field value * @param {Object} startValue The original field value * @param {Ext.grid.Location} The location where actionable mode was successfully started * @since 7.0 */ /** * @event complete * Fires after editing is complete and any changed value has been written to the underlying * field. * @param {Ext.Editor} editor * @param {Object} value The current field value * @param {Object} startValue The original field value * @param {Ext.grid.Location} The location where actionable mode was successfully started * @since 7.0 */ /** * @event canceledit * Fires after editing has been canceled and the editor's value has been reset. * @param {Ext.Editor} editor * @param {Object} value The user-entered field value that was discarded * @param {Object} startValue The original field value that was set back into the editor after * cancel * @since 7.0 */ /** * @event specialkey * Fires when any key related to navigation (arrows, tab, enter, esc, etc.) is pressed. You can * check * {@link Ext.event.Event#getKey} to determine which key was pressed. * @param {Ext.Editor} editor * @param {Ext.form.field.Field} field The field attached to this editor * @param {Ext.event.Event} event The event object * @since 7.0 */ /** * @event beforestaterestore * Fires before the state of the object is restored. * Return false from an event handler to stop the restore. * @param {Ext.grid.Grid} grid this Grid * @param {Object} state The hash of state values returned from the StateProvider. If this * event is not vetoed, then the state object is passed to *`applyColumnState`. * @since 7.6 */ /** * @event staterestore * Fires after the state of the object is restored. * @param {Ext.grid.Grid} grid this Grid * @param {Object} state The hash of state values returned from the StateProvider. * @since 7.6 */ /** * @event beforestatesave * Fires before the state of the object is saved to the configured state provider. * Return false to stop the save. * @param {Ext.grid.Grid} grid this Grid * @param {Object} state The hash of state values. * @since 7.6 */ /** * @event statesave * Fires after the state of the object is saved to the configured state provider. * @param {Ext.grid.Grid} grid this Grid * @param {Object} state The hash of state values. * @since 7.6 */ /** * @private * @readonly * @property {String} [selectionModel=grid] * The selection model type to create. Defaults to `'grid'` for grids. */ selectionModel: 'grid', /** * @property classCls * @inheritdoc */ classCls: Ext.baseCSSPrefix + 'grid', columnLinesCls: Ext.baseCSSPrefix + 'column-lines', /** * @property {Object} columnStateEventMap Column events to be applied * when grid is stateful. * Mapping of this property * `property`: Column event name * `value`: Matching event config */ columnStateEventMap: { columnhide: 'hidden', columnmove: 'weight', columnresize: 'width', columnshow: 'hidden' }, /** * @property {Number} columnStateEventDelay * A buffer to be applied if many state events are fired within a short period. */ columnStateEventDelay: 100, getTemplate: function() { var template = this.callParent(); template.push({ reference: 'resizeMarkerElement', className: Ext.baseCSSPrefix + 'resize-marker-el', hidden: true }); return template; }, beforeInitialize: function() { // In a locking grid assembly, child grids will have an ownerGrid reference. // By default, in a non-locking grid, ownerGrid references this grid. this.ownerGrid = this; this.registerColumnState(); this.callParent(); }, initialize: function() { var me = this, columns = me.getColumns(), persist = me.registeredColumns, titleBar = me.getTitleBar(), headerContainer = me.getHeaderContainer(), scroller = me.getScrollable(), selectable = me.getSelectable(); me.callParent(); if (scroller) { headerContainer.getScrollable().addPartner(scroller, 'x'); } if (titleBar) { me.insert(0, titleBar); } me.add(headerContainer); if (selectable) { selectable.onViewCreated(me); } // In the case of a grid without configured columns, registered columns will never // be added as they are normally added during updateColumns. Here we catch that case // and add all registeredColumns. This issue comes from ExtAngular and how it creates // the grid. if (!columns.length && persist && persist.length) { headerContainer.add(persist); } }, doDestroy: function() { this.destroyMembers('columnsMenu', 'columnsMenuItem', 'rowNumbererColumn'); this.callParent(); }, addColumn: function(column) { return this.getHeaderContainer().add(column); }, addSharedMenuItem: function(sharedItem) { (this.sharedMenuItems || (this.sharedMenuItems = [])).push(sharedItem); }, removeSharedMenuItem: function(sharedItem) { var sharedMenuItems = this.sharedMenuItems; if (sharedMenuItems) { Ext.Array.remove(sharedMenuItems, sharedItem); } }, beforeShowColumnMenu: function(column, menu) { var me = this, i, n, item, sharedMenuItems; me.getColumnsMenuItem(); // ensure "Columns" menu is created and registered sharedMenuItems = me.sharedMenuItems; for (i = 0, n = sharedMenuItems && sharedMenuItems.length; i < n; ++i) { item = sharedMenuItems[i]; item.onBeforeShowColumnMenu(menu, column, me); } return me.fireEvent('beforeshowcolumnmenu', me, column, menu); }, onColumnMenuHide: function(column, menu) { var me = this, sharedMenuItems = me.sharedMenuItems, n = sharedMenuItems && sharedMenuItems.length, i, item; for (i = 0; i < n; ++i) { item = sharedMenuItems[i]; if (!item.destroyed && !item.destroying && !menu.isHidden()) { item.onColumnMenuHide(menu, column, me); } } }, getColumnForField: function(fieldName) { return this.getHeaderContainer().getColumnForField(fieldName); }, /** * Get columns using a selector to filter which columns * to return. * * @param {String/Function} selector * If the selector is a `String`, columns will be found using * {@link Ext.ComponentQuery}. If the selector is a `Function`, * {@link Ext.Array#filter} will be used to filter the columns. * If no selector is provided, all columns will be returned. * @return {Array} */ getColumns: function(selector) { return this.getHeaderContainer().getColumns(selector); }, getVisibleColumns: function() { return this.getHeaderContainer().getVisibleColumns(); }, insertColumn: function(index, column) { return this.getHeaderContainer().insert(index, column); }, insertColumnBefore: function(column, before) { var ret; if (!before) { ret = this.getHeaderContainer().add(column); } else { ret = before.getParent().insertBefore(column, before); } return ret; }, /** * Converts the given parameter to a cell. * @param {HTMLElement/Ext.event.Event/Ext.dom.Element/Ext.data.Model/Ext.grid.Row} value The * value. Can be an event or an element to find the cell via the DOM. Otherwise, a record or * row can be passed. If this occurs, the column parameter also needs to be passed. * @param {Ext.grid.column.Column} [column] The column. Needed if the first parameter is a * model or a row. * @return {Ext.grid.cell.Base} The cell, if it can be found. * * @since 6.5.0 */ mapToCell: function(value, column) { var me = this, ret; if (value) { if (value.isGridCell && value.row.getGrid() === me) { ret = value; } else { if (value.isEntity) { value = me.mapToItem(value); } if (value) { if (value.isGridRow) { column = column || me.getFirstVisibleColumn(); if (column) { ret = value.getCellByColumn(column); } } else { ret = Ext.Component.from(value, me.innerCt, 'gridcellbase'); } } } } return ret || null; }, mapToItem: function(value, as) { if (value && value.isGridCell) { value = value.row; } return this.callParent([value, as]); }, /** * Converts the given parameter to a row body. * @param {Ext.event.Event/Ext.dom.Element/HTMLElement/Ext.data.Model/Ext.grid.Row} value The * value. * Can be an event or an element to find the row body via the DOM. Otherwise, a record or row * can be passed. * @return {Ext.grid.RowBody} The row body, if it can be found. * * @since 6.5.0 */ mapToRowBody: function(value) { if (value) { if (!value.isGridRow) { value = this.mapToItem(value); } if (value && value.isGridRow) { value = value.getBody(); } } return value || null; }, removeColumn: function(column) { return this.getHeaderContainer().remove(column); }, /** * @protected * This method is for use by plugins which require the grid to enter actionable mode * to focus in-cell elements. * * An example of this can be found in the {@link Ext.grid.plugin.CellEditing cell editing} * plugin. * * Actionable plugins have an `{@link Ext.grid.plugin.CellEditing#activateCell activateCell}` * method which will be called whenever the application wants to enter actionable mode * on a certain cell. A {@link Ext.grid.Location grid location} object will be passed. * * The `activateCell` method must return an {@link Ext.grid.Location} if it accepts * control, indicating in its {@link Ext.grid.Location#element element} setting * exactly where focus has moved to. * * Actionable plugins may also expose a `triggerEvent` config which is the name of an * event to be used to trigger actioning that plugin, in addition fo the ARIA standard * method of the user pressing `F2` or `ENTER` when focused on a cell. * * @param {Object} actionable A plugin which creates or manipulates in-cell focusable * elements. */ registerActionable: function(actionable) { this.getNavigationModel().registerActionable(actionable); }, /** * @protected * This method is for use by plugins which require the grid to enter actionable mode * to focus in-cell elements. See {@link #method!registerActionable}. * * @param {Object} actionable The actionable plugin to unregister. */ unregisterActionable: function(actionable) { this.getNavigationModel().unregisterActionable(actionable); }, //------------------------- // Event handlers onColumnAdd: function(container, column, columnIndex) { var me = this, items, ln, i, row; if (!me.initializingColumns && !me.destroying) { items = me.items.items; ln = items.length; for (i = 0; i < ln; i++) { row = items[i]; if (row.hasGridCells) { row.insertColumn(columnIndex, column); } } me.onColumnChange('columnadd', [me, column, columnIndex]); } // Update column state, if new column is added if (!me.isConfiguring && column.headerId == null) { me.onStateChange(); me.applyColumnState(); } }, onColumnHide: function(container, column) { var me = this, items, ln, i, row; if (me.initialized && !me.destroying) { items = me.items.items; ln = items.length; for (i = 0; i < ln; i++) { row = items[i]; if (row.hasGridCells) { row.hideColumn(column); } } me.onColumnChange('columnhide', [me, column]); } }, onColumnMove: function(container, columns, group, fromIdx) { var me = this, before = null, colLen = columns.length, items, ln, i, j, row, column, index, leaves; if (me.initialized && !me.destroying) { items = me.items.items; ln = items.length; // Find the item that will be after the last leaf we're going to insert // Don't bother checking the array bounds, if it goes out of bounds then // null is the right answer leaves = me.getHeaderContainer().getColumns(); index = leaves.indexOf(columns[colLen - 1]); before = leaves[index + 1] || null; for (i = colLen - 1; i >= 0; --i) { column = columns[i]; for (j = 0; j < ln; j++) { row = items[j]; if (row.hasGridCells) { row.insertColumnBefore(column, before); } } me.onColumnChange('columnmove', [me, column, fromIdx + i, leaves.indexOf(column)]); before = column; } } }, onColumnRemove: function(container, column) { var me = this, items, ln, i, row; if (me.initialized && !me.destroying) { if (column === me.sortedColumn) { me.sortedColumn = null; } items = me.items.items; ln = items.length; for (i = 0; i < ln; i++) { row = items[i]; if (row.hasGridCells) { row.removeColumn(column); } } me.onColumnChange('columnremove', [me, column]); } }, onColumnResize: function(container, column, width, oldWidth) { var me = this; if (!me.destroying) { // Will be null on the first time if (oldWidth && !column.getHidden()) { if (me.infinite) { me.refreshScrollerSize(); } me.fireEvent('columnresize', me, column, width); } } }, onColumnShow: function(container, column) { var me = this, items, ln, i, row; if (me.initialized && !me.destroying) { items = me.items.items; ln = items.length; for (i = 0; i < ln; i++) { row = items[i]; if (row.hasGridCells) { row.showColumn(column); } } me.onColumnChange('columnshow', [me, column]); } }, onColumnSort: function(container, column, direction) { this.fireEvent('columnsort', this, column, direction); }, onRender: function() { var hideHeaders = this._hideHeaders; this.callParent(); // hideHeaders requires measure, so must be done on render if (hideHeaders) { this.updateHideHeaders(hideHeaders); } }, applyColumns: function(columns) { if (this.isColumnsStateful()) { // do not carry state info to the column data columns = Ext.clone(columns); } return columns; }, privates: { dataItemMap: { header: 1, footer: 1 }, // column state header id separator for the nested group headerIdSeparator: '-', handleStoreSort: function() { if (this.rendered) { this.getHeaderContainer().setSortState(); } }, onStoreGroupChange: function(store, grouper) { this.callParent([store, grouper]); this.handleStoreSort(); }, onStoreSort: function() { this.handleStoreSort(); }, registerColumn: function(column) { var me = this, columns = me.registeredColumns, headerCt = me.getHeaderContainer(); if (!column.isGridColumn) { column = Ext.create(column); } if (!columns) { me.registeredColumns = columns = []; } columns.push(column); // We may have already configured the columns, even if we are // configuring, so check if we have items if (!me.isConfiguring || (headerCt && headerCt.items.getCount())) { headerCt.add(column); } return column; }, unregisterColumn: function(column, destroy) { var columns = this.registeredColumns, headerCt = this.getHeaderContainer(); if (!this.destroying) { if (columns) { Ext.Array.remove(columns, column); } if (headerCt) { headerCt.remove(column, destroy === true); } } return column; }, /** * @private * We MUST use our own cells as delegates for grid-based events. * Cell events will not work without this. The event system would not * carry cell information if we don't delegate onto our cells. */ generateSelectorFunctions: function() { var me = this; me.callParent(); // This is used solely by the view event listener to filter the event reactions // to the level of granularity needed. // At the Grid level, this will be cell elements. me.eventDelegate = function(candidate) { var comp = Ext.Component.from(candidate), ret = true, row; // Don't fire child events for the grid itself if (!comp || comp === me) { return false; } // If it's a direct child of the grid, and it's a row or header/footer, it's ok if (comp.getRefOwner() === me) { ret = comp.isGridRow || me.dataItemMap[comp.$dataItem]; } else { // Otherwise, this is to check for either: // a) cell // b) a row body // // We don't want to fire events for things inside the row body, or items inside // cells row = comp.row; // GroupHeaders and GroupFooters are created at the List class level // so they do not get a "grid" upward link, so check their "list" upward link. ret = row && row.isGridRow && (row.grid || row.list) === me; } return ret; }; }, getFirstVisibleColumn: function() { var columns = this.getVisibleColumns(); return columns.length ? columns[0] : null; }, getLastVisibleColumn: function() { var columns = this.getVisibleColumns(), len = columns.length; return len ? columns[len - 1] : null; }, isFirstVisibleColumn: function(column) { return this.getFirstVisibleColumn() === column; }, isLastVisibleColumn: function(column) { return this.getLastVisibleColumn() === column; }, createDataItem: function(cfg) { var item = this.callParent([cfg]); item.grid = this; return item; }, // ----------------------- // Event handlers onColumnChange: function(changeEvent, eventArgs) { var me = this; // Total width will change upon add/remove/hide/show // So keep innerCt size synced if (changeEvent !== 'columnmove' && changeEvent !== 'columnadd' && changeEvent !== 'columnremove') { me.refreshInnerWidth(); } if (!me.isConfiguring) { me.fireEventArgs(changeEvent, eventArgs); } me.clearItemCaches(); // TODO: This may cause a change in row heights, currently should // be handled by using variableHeights, but the grid could re-measure as // needed // this.refreshScrollerSize(); }, refreshInnerWidth: function() { var body = this.getHeaderContainer().bodyElement.dom; this.setInnerWidth(Math.max(body.scrollWidth, body.clientWidth)); }, onColumnComputedWidthChange: function(changedColumns, totalColumnWidth) { var me = this, groupingInfo = me.groupingInfo; if (!me.destroying) { // Set the item containing element to the correct width. me.setInnerWidth(totalColumnWidth); me.setCellSizes(changedColumns, me.items.items); me.setCellSizes(changedColumns, me.itemCache); if (me.isGrouping()) { me.setCellSizes(changedColumns, groupingInfo.header.unused); me.setCellSizes(changedColumns, groupingInfo.footer.unused); } // Row sizing rules change if we have flexed columns. if (me.hasListeners.columnlayout) { me.fireEvent('columnlayout', me, changedColumns, totalColumnWidth); } } }, onCellSelect: function(location) { var cell = location.getCell(); if (cell) { cell.addCls(this.selectedCls); } }, onCellDeselect: function(location) { var cell = location.getCell(); if (cell) { cell.removeCls(this.selectedCls); } }, setCellSizes: function(changedColumns, items) { var len = items.length, changedColCount = changedColumns.length, row, i, j; // Size the cells for (i = 0; i < len; i++) { row = items[i]; if (row.hasGridCells) { for (j = 0; j < changedColCount; j++) { row.setColumnWidth(changedColumns[j]); } } } }, // ----------------------- // Configs // columnLines updateColumnLines: function(columnLines) { this.el.toggleCls(this.columnLinesCls, columnLines); }, // columnResize updateColumnResize: function(enabled) { var me = this, plugin = me.findPlugin('columnresizing'); if (!plugin) { if (enabled) { me.addPlugin('columnresizing'); } } else { plugin.setGrid(enabled ? me : null); } }, // columns updateColumns: function(columns) { var me = this, header = me.getHeaderContainer(), count = columns && columns.length, persist = me.registeredColumns; // If the header container is an instance, then it's already // peeked at the columns config and included it, so bail out if (header) { // With a new column set, the rowHeight must be invalidated. // The new columns may bring in a different data shape. me.rowHeight = null; if (header) { header.beginColumnUpdate(); if (header.getItems().getCount()) { // Preserve persistent columns if (persist) { header.remove(persist, false); } // Also preserve any returning columns... if (count) { header.remove(columns.filter(function(col) { return col.isInstance; }), /* destroy= */ false); } header.removeAll(/* destroy= */ true, /* everything= */ true); } if (count) { me.initializingColumns = me.isConfiguring; // Update column state property if (me.isColumnsStateful()) { // calculate column state header id me.updateColumnStateProp(columns); // update column config with state data // and re-arrange, if it moved to a different header group me.adjustColumnFromState(columns); } header.setColumns(columns); // Re-add any persistent columns, any adjusted weights are recalculated if (persist) { header.add(persist); } delete me.initializingColumns; // TODO: This may cause a change in row heights, currently should // be handled by using variableHeights, but the grid could re-measure as // needed // me.refreshScrollerSize(); } header.endColumnUpdate(); } } }, applyStore: function(store, oldStore) { var me = this, ret = me.callParent([ store, oldStore ]); //<debug> if (ret && ret.isVirtualStore && me.getGrouped()) { Ext.Logger.warn('Virtual store does not suppport grouping'); } //</debug> return ret; }, applyRowNumbers: function(rowNumbers) { var me = this; if (rowNumbers) { rowNumbers = me.rowNumbererColumn = Ext.create(Ext.apply({ xtype: 'rownumberer', weight: -1000, editRenderer: me.renderEmpty }, rowNumbers)); } return rowNumbers; }, updateRowNumbers: function(rowNumbers, oldRowNumbers) { if (oldRowNumbers) { this.unregisterColumn(oldRowNumbers, true); } if (rowNumbers) { this.registerColumn(rowNumbers); } }, renderEmpty: function() { return '\xA0'; }, // columnsMenuItem applyColumnsMenuItem: function(config, existing) { return Ext.updateWidget(existing, config, this, 'createColumnsMenuItem'); }, createColumnsMenuItem: function(config) { return Ext.apply({ grid: this }, config); }, // headerContainer applyHeaderContainer: function(config, existing) { return Ext.updateWidget(existing, config, this, 'createHeaderContainer'); // // if (headerContainer && !headerContainer.isComponent) { // headerContainer = Ext.factory(Ext.apply({ // sortable: this.getSortable(), // grid: this // }, headerContainer), Ext.grid.HeaderContainer); // } // // return headerContainer; }, createHeaderContainer: function(config) { config = this.mergeProxiedConfigs('headerContainer', config, /* alwaysClone= */ true); config.sortable = this.getSortable(); config.grid = this; return config; }, updateHeaderContainer: function(headerContainer) { if (headerContainer) { // TODO just call these methods directly from rootHeaderCt? // the old headerContainers are destroyed if they are replaced... headerContainer.on({ columnresize: 'onColumnResize', columnshow: 'onColumnShow', columnhide: 'onColumnHide', columnadd: 'onColumnAdd', columnmove: 'onColumnMove', columnremove: 'onColumnRemove', columnsort: 'onColumnSort', columngroupremove: 'onColumnGroupRemove', scope: this }); } }, // hideHeaders updateHideHeaders: function(hideHeaders) { if (this.rendered) { // eslint-disable-next-line vars-on-top var headerContainer = this.getHeaderContainer(); // To hide the headers, just pull the following element upwards to cover it if (hideHeaders) { headerContainer.el.setStyle({ visibility: 'hidden', marginBottom: '-' + headerContainer.el.measure('h') + 'px' }); } else { headerContainer.el.setStyle({ visibility: '', marginBottom: '' }); } } }, updateHideScrollbar: function(hide) { var w = Ext.scrollbar.width(); this.element.setStyle('margin-right', hide ? -w + 'px' : ''); }, // title updateTitle: function(title) { var titleBar = this.getTitleBar(); if (titleBar) { if (title) { titleBar.setTitle(title); if (titleBar.isHidden()) { titleBar.show(); } } else { titleBar.hide(); } } }, // titleBar applyTitleBar: function(config, existing) { return Ext.updateWidget(existing, config); }, updateTitleBar: function(titleBar) { if (titleBar && !titleBar.getTitle()) { titleBar.setTitle(this.getTitle()); } }, // totalColumnWidth applyTotalColumnWidth: function(totalColumnWidth) { var rows = this.dataItems; // If we don't have any items yet, wait return rows.length === 0 ? undefined : totalColumnWidth; }, // verticalOverflow updateVerticalOverflow: function(value, was) { var headerContainer = this.getHeaderContainer(), summaryRow = this.findPlugin('summaryrow') || this.findPlugin('gridsummaryrow'), scrollable = this.getScrollable(), verticalScrollbarWidth = scrollable && scrollable.getScrollbarSize().width, y = !!(scrollable && scrollable.getY()), addOverflow = y && verticalScrollbarWidth > 0 && value, row; this.callParent([value, was]); // TODO: refactor this code within headerContainer and summaryRow headerContainer.setVerticalOverflow(addOverflow); if (summaryRow) { row = summaryRow.getRow(); if (!row.destroyed) { row.rightSpacer.setStyle({ width: (addOverflow ? (verticalScrollbarWidth - 1) : 0) + 'px' }); } } }, // Enable column reorder updateEnableColumnMove: function(enabled) { var me = this, plugin = me.findPlugin('headerreorder'); if (!plugin && enabled) { plugin = me.addPlugin('headerreorder'); } if (plugin) { plugin.setGrid(enabled ? me : null); } }, /** * @method getSelection * Returns the grid's selection if {@link Ext.grid.selection.Model#cfg!mode mode} is single * @return {Ext.data.Model} returns selected record if selectable is rows * @return {Ext.grid.column.Column} returns selected column if selectable is columns * @return {Ext.data.Model} returns selected record if selectable is cells * * Returns the last selected column/cell's record/row's record based on selectable * if {@link Ext.grid.selection.Model#cfg!mode mode} is multi */ getSelection: function() { var me = this, selectable = me.getSelectable(), selection = selectable.getSelection(), selectionType = selection.type; if (selectionType === 'columns') { return selection.lastColumnSelected; } if (selectionType === 'cells') { return selection.endCell && selection.endCell.record; } return this.callParent(); }, /** * Register events for column state persistance. * @private */ registerColumnState: function() { var me = this, eventsMap = me.columnStateEventMap, eventObj = {}, key; if (!me.isColumnsStateful()) { return; } for (key in eventsMap) { eventObj[key] = 'onStateChange'; } eventObj.scope = me; eventObj.buffer = me.columnStateEventDelay; me.on(eventObj); }, /** * Handler for {@link #columnStateEventMap} events. * @private */ onStateChange: function() { var me = this, state, items; if (!me.isColumnsStateful()) { return; } state = me.columnStateData || {}; items = me.getHeaderContainer().items; me.updateColumnStateProp(items.items); me.columnStateData = me.calculateColumnState(items, state); me.persistColumnState(me.columnStateData); }, /** * Iterate through each column and assign headerId and item group move. * @param {Ext.grid.column.Column[]/Object[]} columns {@link #columns} * @param {String} ownerHeaderId Column Header Id * @private */ updateColumnStateProp: function(columns, ownerHeaderId) { var i, column; ownerHeaderId = ownerHeaderId ? (ownerHeaderId + this.headerIdSeparator) : ''; for (i = 0; i < columns.length; i++) { column = columns[i]; column.headerId = this.getColumnStateId(column) || (ownerHeaderId + 'h' + i); // re-iterate for header group if (column.isHeaderGroup || !Ext.isEmpty(column.columns)) { this.updateColumnStateProp(column.innerItems || column.columns, column.headerId); } } return columns; }, /** * Read all the column state. * @param {Ext.grid.column.Column[]} columns {@link #columns} * @param {Object[]} columnsState Column State * @private */ calculateColumnState: function(columns, columnsState) { var me = this, eventsMap = me.columnStateEventMap, key, cfg, prop, hId, value; columns.each(function(column) { // don't calculate column state if it's explicitly set to false if (column.getStateful() === false) { return; } hId = me.getColumnStateId(column); // Locked Grid: If column is moved to different region, // store data to the region specific prefixed ID if (column.region && column.region !== column.regionKey) { hId = column.regionKey + ':' + hId; } columnsState[hId] = columnsState[hId] || {}; for (key in eventsMap) { prop = eventsMap[key]; cfg = column.self.$config.configs[prop]; if (cfg) { value = column[cfg.names.get](); // do not store the state property, if it's not changed if (!Ext.isEmpty(value)) { columnsState[hId][prop] = value; } } } // assign parent header id to the column, if it's inside nested column delete columnsState[hId].parentHeaderId; if (column.parent && column.parent.headerId) { columnsState[hId].parentHeaderId = column.parent.headerId; } // Locked Grid - Pass column new region key info to the state data if (column.region) { columnsState[hId].region = column.region; } else { // remove saved region from state data, if column is moved back to // its original state delete columnsState[hId].region; } // get column order on the nested group if (column.isHeaderGroup) { me.calculateColumnState(column.items, columnsState); } }); return columnsState; }, /** * Reposition columns from the saved state. * @param {Object[]} columns {@link #columns} * @private */ adjustColumnFromState: function(columns) { var me = this, stateData; if (!me.isColumnsStateful()) { return; } stateData = me.getColumnState(); if (Ext.Object.isEmpty(stateData)) { return; } if (me.fireEvent('beforestaterestore', me, stateData) === false) { return; } me.updateColumnStateBeforeAdd(stateData, columns); me.fireEvent('staterestore', me, stateData); }, /** * Return matched column from the group * @private */ findColumnByParentHeaderId: function(columns, column) { var i, item, matched, innerItems; for (i = 0; i < columns.length; i++) { item = columns[i] || columns.items[i]; if (item.headerId === column.parentHeaderId) { return item; } else if (!Ext.isEmpty(item.columns) || item.isHeaderGroup) { innerItems = item.isGridColumn ? item.innerItems : item.columns; matched = this.findColumnByParentHeaderId(innerItems, column); if (matched) { return matched; } } } }, /** * Update column config with saved state data * @private */ updateColumnStateBeforeAdd: function(columnsState, columns) { var me = this, i, column, data; if (!me.isColumnsStateful() || Ext.isEmpty(columns)) { return; } for (i = 0; i < columns.length; i++) { column = columns[i]; data = columnsState[column.headerId]; if (data) { Ext.apply(column, data); if (!Ext.isEmpty(column.columns)) { me.updateColumnStateBeforeAdd(columnsState, column.columns); } } } // re-arrange column item based on it's parent header ID me.adjustNestedStateBeforeAdd(columns); }, /** * If column item is moved to different header, move the item * @private */ adjustNestedStateBeforeAdd: function(columns) { var me = this, i, column, matched, mainColumns; for (i = 0; i < columns.length; i++) { column = columns[i]; mainColumns = me._columns; if (!column.parentHeaderId) { // Move nested column to root header container if (mainColumns.indexOf(column) === -1) { me.rearrangeColumn(mainColumns, columns, column); // re-iterate the loop to have correct column order after move i = -1; } continue; } matched = me.findColumnByParentHeaderId(columns, column); if (matched && Ext.isArray(matched.columns)) { me.rearrangeColumn(matched.columns, columns, column); // re-iterate the loop to have correct column order after move i = -1; } else { // move deep nested column to its moved header container matched = me.findColumnByParentHeaderId(mainColumns, column); if (matched && Ext.isArray(matched.columns) && matched.columns.indexOf(column) === -1) { me.rearrangeColumn(matched.columns, columns, column); // re-iterate the loop to have correct column order after move i = -1; } } } }, rearrangeColumn: function(matched, columns, column) { Ext.Array.insert(matched, column.weight, [column]); Ext.Array.removeAt(columns, columns.indexOf(column)); }, /** * Reposition columns from the saved state. * @param {Object} columnsState Columns state data. * @param {Ext.grid.column.Column[]/Object[]} columns {@link #columns} * @private */ applyColumnState: function(columnsState, columns) { var me = this, eventsMap = me.columnStateEventMap, data, key, prop, cfg; if (!me.isColumnsStateful()) { return; } columnsState = columnsState || me.getColumnState(); if (Ext.Object.isEmpty(columnsState)) { return; } if (Ext.isEmpty(columns)) { columns = me.getHeaderContainer().items; } columns.each(function(column) { // Locked Grid - do not update the moved column property if (column.region) { return; } data = columnsState[me.getColumnStateId(column)]; if (data) { for (key in eventsMap) { prop = eventsMap[key]; // ignore weight property to be set, if it has different new group if (Ext.isEmpty(data[prop]) || prop === 'weight' && data.parentHeaderId !== me.getColumnStateId(column.parent)) { continue; } cfg = column.self.$config.configs[prop]; if (cfg) { column[cfg.names.set](data[prop]); } } if (column.isHeaderGroup) { me.applyColumnState(columnsState, column.items); } } }); me.adjustNestedState(columns, columnsState); }, adjustNestedState: function(columns, columnsState) { var matched, pId, stateItem, headerCt; columns.each(function(column) { pId = column.parentHeaderId; stateItem = columnsState[column.headerId]; if (stateItem) { pId = stateItem.parentHeaderId; column.parentHeaderId = pId; } if (!pId) { headerCt = column.getRootHeaderCt(); // Move nested column to root header container, if dragged out side if (headerCt.indexOf(column) === -1) { headerCt.insert(stateItem.weight, column); // re-run the loop if column gets re-arranged. this.adjustNestedState(columns, columnsState); return false; } return; } matched = this.findColumnByParentHeaderId(columns, column); if (matched && matched.isHeaderGroup) { matched.insert(stateItem.weight, column); // re-run the loop if column gets re-arranged. this.adjustNestedState(columns, columnsState); return false; } }, this); }, /** * Return {Object} saved column state. * @private */ getColumnState: function() { var me = this, state, provider, id; if (!me.isColumnsStateful()) { return; } if (me.columnStateData) { return me.columnStateData; } state = me.getStateBuilder(); if (state) { provider = Ext.state.Provider.get(); id = state.root.id + '-column-state'; return provider.get(id); } }, /** * Save column state. * @param {Object} columnsState Column State data * @private */ persistColumnState: function(columnsState) { var me = this, state, provider, id; if (me.fireEvent('beforestatesave', me, columnsState) === false) { return; } state = me.getStateBuilder(); if (state) { provider = Ext.state.Provider.get(); id = state.root.id + '-column-state'; provider.set(id, columnsState); me.fireEvent('statesave', me, columnsState); } }, /** * Checks if grid is stateful and has column state map events * @returns {Boolean} */ isColumnsStateful: function() { var me = this, stateId = me.getStateId(), stateful = me.config.stateful, eventsMap = me.columnStateEventMap; return !(!stateId || !stateful || Ext.Object.isEmpty(eventsMap)); }, /** * Manage state for the header group remove */ onColumnGroupRemove: function(headerCt, column) { var me = this, id, region; if (!me.isColumnsStateful()) { return; } region = me.region; id = me.getColumnStateId(column); // Locked Grid: notify if header group is removed if (region && region.isLockedGridRegion) { me.fireEvent('regioncolumngroupremove', column, id); return; } column.setHidden(true); me.onStateChange(); }, getColumnStateId: function(column) { return column.stateId || column._stateId || column.headerId; } } // privates }, function(Grid) { Grid.prototype.indexModifiedFields = Ext.Array.toMap;});