X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/6746dc89c47ed01b165cc1152533605f97eb8e8d..HEAD:/docs/source/Container2.html?ds=inline diff --git a/docs/source/Container2.html b/docs/source/Container2.html index 2240c5f8..d54c25b1 100644 --- a/docs/source/Container2.html +++ b/docs/source/Container2.html @@ -3,8 +3,8 @@
/** - * @class Ext.container.Container - * @extends Ext.container.AbstractContainer - * <p>Base class for any {@link Ext.Component} that may contain other Components. Containers handle the - * basic behavior of containing items, namely adding, inserting and removing items.</p> +/** + * @class Ext.grid.header.Container + * @extends Ext.container.Container * - * <p>The most commonly used Container classes are {@link Ext.panel.Panel}, {@link Ext.window.Window} and {@link Ext.tab.Panel}. - * If you do not need the capabilities offered by the aforementioned classes you can create a lightweight - * Container to be encapsulated by an HTML element to your specifications by using the - * <code><b>{@link Ext.Component#autoEl autoEl}</b></code> config option.</p> - * - * {@img Ext.Container/Ext.Container.png Ext.Container component} - * <p>The code below illustrates how to explicitly create a Container:<pre><code> -// explicitly create a Container -Ext.create('Ext.container.Container', { - layout: { - type: 'hbox' - }, - width: 400, - renderTo: Ext.getBody(), - border: 1, - style: {borderColor:'#000000', borderStyle:'solid', borderWidth:'1px'}, - defaults: { - labelWidth: 80, - // implicitly create Container by specifying xtype - xtype: 'datefield', - flex: 1, - style: { - padding: '10px' - } - }, - items: [{ - xtype: 'datefield', - name: 'startDate', - fieldLabel: 'Start date' - },{ - xtype: 'datefield', - name: 'endDate', - fieldLabel: 'End date' - }] -}); -</code></pre></p> - * - * <p><u><b>Layout</b></u></p> - * <p>Container classes delegate the rendering of child Components to a layout - * manager class which must be configured into the Container using the - * <code><b>{@link #layout}</b></code> configuration property.</p> - * <p>When either specifying child <code>{@link #items}</code> of a Container, - * or dynamically {@link #add adding} Components to a Container, remember to - * consider how you wish the Container to arrange those child elements, and - * whether those child elements need to be sized using one of Ext's built-in - * <b><code>{@link #layout}</code></b> schemes. By default, Containers use the - * {@link Ext.layout.container.Auto Auto} scheme which only - * renders child components, appending them one after the other inside the - * Container, and <b>does not apply any sizing</b> at all.</p> - * <p>A common mistake is when a developer neglects to specify a - * <b><code>{@link #layout}</code></b> (e.g. widgets like GridPanels or - * TreePanels are added to Containers for which no <code><b>{@link #layout}</b></code> - * has been specified). If a Container is left to use the default - * {Ext.layout.container.Auto Auto} scheme, none of its - * child components will be resized, or changed in any way when the Container - * is resized.</p> - * <p>Certain layout managers allow dynamic addition of child components. - * Those that do include {@link Ext.layout.container.Card}, - * {@link Ext.layout.container.Anchor}, {@link Ext.layout.container.VBox}, {@link Ext.layout.container.HBox}, and - * {@link Ext.layout.container.Table}. For example:<pre><code> -// Create the GridPanel. -var myNewGrid = new Ext.grid.Panel({ - store: myStore, - headers: myHeaders, - title: 'Results', // the title becomes the title of the tab -}); - -myTabPanel.add(myNewGrid); // {@link Ext.tab.Panel} implicitly uses {@link Ext.layout.container.Card Card} -myTabPanel.{@link Ext.tab.Panel#setActiveTab setActiveTab}(myNewGrid); - * </code></pre></p> - * <p>The example above adds a newly created GridPanel to a TabPanel. Note that - * a TabPanel uses {@link Ext.layout.container.Card} as its layout manager which - * means all its child items are sized to {@link Ext.layout.container.Fit fit} - * exactly into its client area. - * <p><b><u>Overnesting is a common problem</u></b>. - * An example of overnesting occurs when a GridPanel is added to a TabPanel - * by wrapping the GridPanel <i>inside</i> a wrapping Panel (that has no - * <code><b>{@link #layout}</b></code> specified) and then add that wrapping Panel - * to the TabPanel. The point to realize is that a GridPanel <b>is</b> a - * Component which can be added directly to a Container. If the wrapping Panel - * has no <code><b>{@link #layout}</b></code> configuration, then the overnested - * GridPanel will not be sized as expected.<p> - * - * <p><u><b>Adding via remote configuration</b></u></p> - * - * <p>A server side script can be used to add Components which are generated dynamically on the server. - * An example of adding a GridPanel to a TabPanel where the GridPanel is generated by the server - * based on certain parameters: - * </p><pre><code> -// execute an Ajax request to invoke server side script: -Ext.Ajax.request({ - url: 'gen-invoice-grid.php', - // send additional parameters to instruct server script - params: { - startDate: Ext.getCmp('start-date').getValue(), - endDate: Ext.getCmp('end-date').getValue() - }, - // process the response object to add it to the TabPanel: - success: function(xhr) { - var newComponent = eval(xhr.responseText); // see discussion below - myTabPanel.add(newComponent); // add the component to the TabPanel - myTabPanel.setActiveTab(newComponent); - }, - failure: function() { - Ext.Msg.alert("Grid create failed", "Server communication failure"); - } -}); -</code></pre> - * <p>The server script needs to return a JSON representation of a configuration object, which, when decoded - * will return a config object with an {@link Ext.Component#xtype xtype}. The server might return the following - * JSON:</p><pre><code> -{ - "xtype": 'grid', - "title": 'Invoice Report', - "store": { - "model": 'Invoice', - "proxy": { - "type": 'ajax', - "url": 'get-invoice-data.php', - "reader": { - "type": 'json' - "record": 'transaction', - "idProperty": 'id', - "totalRecords": 'total' - }) - }, - "autoLoad": { - "params": { - "startDate": '01/01/2008', - "endDate": '01/31/2008' - } - } - }, - "headers": [ - {"header": "Customer", "width": 250, "dataIndex": 'customer', "sortable": true}, - {"header": "Invoice Number", "width": 120, "dataIndex": 'invNo', "sortable": true}, - {"header": "Invoice Date", "width": 100, "dataIndex": 'date', "renderer": Ext.util.Format.dateRenderer('M d, y'), "sortable": true}, - {"header": "Value", "width": 120, "dataIndex": 'value', "renderer": 'usMoney', "sortable": true} - ] -} -</code></pre> - * <p>When the above code fragment is passed through the <code>eval</code> function in the success handler - * of the Ajax request, the result will be a config object which, when added to a Container, will cause instantiation - * of a GridPanel. <b>Be sure that the Container is configured with a layout which sizes and positions the child items to your requirements.</b></p> - * <p>Note: since the code above is <i>generated</i> by a server script, the <code>autoLoad</code> params for - * the Store, the user's preferred date format, the metadata to allow generation of the Model layout, and the ColumnModel - * can all be generated into the code since these are all known on the server.</p> + * Container which holds headers and is docked at the top or bottom of a TablePanel. + * The HeaderContainer drives resizing/moving/hiding of columns within the TableView. + * As headers are hidden, moved or resized the headercontainer is responsible for + * triggering changes within the view. */ -Ext.define('Ext.container.Container', { - extend: 'Ext.container.AbstractContainer', - alias: 'widget.container', - alternateClassName: 'Ext.Container', - - /** - * Return the immediate child Component in which the passed element is located. - * @param el The element to test. - * @return {Component} The child item which contains the passed element. - */ - getChildByElement: function(el) { - var item, - itemEl, +Ext.define('Ext.grid.header.Container', { + extend: 'Ext.container.Container', + uses: [ + 'Ext.grid.ColumnLayout', + 'Ext.grid.column.Column', + 'Ext.menu.Menu', + 'Ext.menu.CheckItem', + 'Ext.menu.Separator', + 'Ext.grid.plugin.HeaderResizer', + 'Ext.grid.plugin.HeaderReorderer' + ], + border: true, + + alias: 'widget.headercontainer', + + baseCls: Ext.baseCSSPrefix + 'grid-header-ct', + dock: 'top', + + /** + * @cfg {Number} weight + * HeaderContainer overrides the default weight of 0 for all docked items to 100. + * This is so that it has more priority over things like toolbars. + */ + weight: 100, + defaultType: 'gridcolumn', + /** + * @cfg {Number} defaultWidth + * Width of the header if no width or flex is specified. Defaults to 100. + */ + defaultWidth: 100, + + + sortAscText: 'Sort Ascending', + sortDescText: 'Sort Descending', + sortClearText: 'Clear Sort', + columnsText: 'Columns', + + lastHeaderCls: Ext.baseCSSPrefix + 'column-header-last', + firstHeaderCls: Ext.baseCSSPrefix + 'column-header-first', + headerOpenCls: Ext.baseCSSPrefix + 'column-header-open', + + // private; will probably be removed by 4.0 + triStateSort: false, + + ddLock: false, + + dragging: false, + + /** + * <code>true</code> if this HeaderContainer is in fact a group header which contains sub headers. + * @type Boolean + * @property isGroupHeader + */ + + /** + * @cfg {Boolean} sortable + * Provides the default sortable state for all Headers within this HeaderContainer. + * Also turns on or off the menus in the HeaderContainer. Note that the menu is + * shared across every header and therefore turning it off will remove the menu + * items for every header. + */ + sortable: true, + + initComponent: function() { + var me = this; + + me.headerCounter = 0; + me.plugins = me.plugins || []; + + // TODO: Pass in configurations to turn on/off dynamic + // resizing and disable resizing all together + + // Only set up a Resizer and Reorderer for the topmost HeaderContainer. + // Nested Group Headers are themselves HeaderContainers + if (!me.isHeader) { + me.resizer = Ext.create('Ext.grid.plugin.HeaderResizer'); + me.reorderer = Ext.create('Ext.grid.plugin.HeaderReorderer'); + if (!me.enableColumnResize) { + me.resizer.disable(); + } + if (!me.enableColumnMove) { + me.reorderer.disable(); + } + me.plugins.push(me.reorderer, me.resizer); + } + + // Base headers do not need a box layout + if (me.isHeader && !me.items) { + me.layout = 'auto'; + } + // HeaderContainer and Group header needs a gridcolumn layout. + else { + me.layout = { + type: 'gridcolumn', + availableSpaceOffset: me.availableSpaceOffset, + align: 'stretchmax', + resetStretch: true + }; + } + me.defaults = me.defaults || {}; + Ext.applyIf(me.defaults, { + width: me.defaultWidth, + triStateSort: me.triStateSort, + sortable: me.sortable + }); + me.callParent(); + me.addEvents( + /** + * @event columnresize + * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. + * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition + * @param {Number} width + */ + 'columnresize', + + /** + * @event headerclick + * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. + * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition + * @param {Ext.EventObject} e + * @param {HTMLElement} t + */ + 'headerclick', + + /** + * @event headertriggerclick + * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. + * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition + * @param {Ext.EventObject} e + * @param {HTMLElement} t + */ + 'headertriggerclick', + + /** + * @event columnmove + * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. + * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition + * @param {Number} fromIdx + * @param {Number} toIdx + */ + 'columnmove', + /** + * @event columnhide + * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. + * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition + */ + 'columnhide', + /** + * @event columnshow + * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. + * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition + */ + 'columnshow', + /** + * @event sortchange + * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers. + * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition + * @param {String} direction + */ + 'sortchange', + * @event menucreate + * Fired immediately after the column header menu is created. + * @param {Ext.grid.header.Container} ct This instance + * @param {Ext.menu.Menu} menu The Menu that was created + */ + 'menucreate' + ); + }, + + onDestroy: function() { + Ext.destroy(this.resizer, this.reorderer); + this.callParent(); + }, + + applyDefaults: function(config){ + /* + * Ensure header.Container defaults don't get applied to a RowNumberer + * if an xtype is supplied. This isn't an ideal solution however it's + * much more likely that a RowNumberer with no options will be created, + * wanting to use the defaults specified on the class as opposed to + * those setup on the Container. + */ + if (config && !config.isComponent && config.xtype == 'rownumberer') { + return config; + } + return this.callParent([config]); + }, + + applyColumnsState: function(columns) { + if (!columns || !columns.length) { + return; + } + + var me = this, + i = 0, + index, + col; + + Ext.each(columns, function (columnState) { + col = me.down('gridcolumn[headerId=' + columnState.id + ']'); + if (col) { + index = me.items.indexOf(col); + if (i !== index) { + me.moveHeader(index, i); + } + + if (col.applyColumnState) { + col.applyColumnState(columnState); + } + ++i; + } + }); + }, + + getColumnsState: function () { + var me = this, + columns = [], + state; + + me.items.each(function (col) { + state = col.getColumnState && col.getColumnState(); + if (state) { + columns.push(state); + } + }); + + return columns; + }, + + // Invalidate column cache on add + // We cannot refresh the View on every add because this method is called + // when the HeaderDropZone moves Headers around, that will also refresh the view + onAdd: function(c) { + var me = this; + if (!c.headerId) { + c.headerId = c.initialConfig.id || ('h' + (++me.headerCounter)); + } + //<debug warn> + if (Ext.global.console && Ext.global.console.warn) { + if (!me._usedIDs) me._usedIDs = {}; + if (me._usedIDs[c.headerId]) { + Ext.global.console.warn(this.$className, 'attempted to reuse an existing id', c.headerId); + } + me._usedIDs[c.headerId] = true; + } + //</debug> + me.callParent(arguments); + me.purgeCache(); + }, + + // Invalidate column cache on remove + // We cannot refresh the View on every remove because this method is called + // when the HeaderDropZone moves Headers around, that will also refresh the view + onRemove: function(c) { + var me = this; + me.callParent(arguments); + me.purgeCache(); + }, + + afterRender: function() { + this.callParent(); + var store = this.up('[store]').store, + sorters = store.sorters, + first = sorters.first(), + hd; + + if (first) { + hd = this.down('gridcolumn[dataIndex=' + first.property +']'); + if (hd) { + hd.setSortState(first.direction, false, true); + } + } + }, + + afterLayout: function() { + if (!this.isHeader) { + var me = this, + topHeaders = me.query('>gridcolumn:not([hidden])'), + viewEl, + firstHeaderEl, + lastHeaderEl; + + me.callParent(arguments); + + if (topHeaders.length) { + firstHeaderEl = topHeaders[0].el; + if (firstHeaderEl !== me.pastFirstHeaderEl) { + if (me.pastFirstHeaderEl) { + me.pastFirstHeaderEl.removeCls(me.firstHeaderCls); + } + firstHeaderEl.addCls(me.firstHeaderCls); + me.pastFirstHeaderEl = firstHeaderEl; + } + + lastHeaderEl = topHeaders[topHeaders.length - 1].el; + if (lastHeaderEl !== me.pastLastHeaderEl) { + if (me.pastLastHeaderEl) { + me.pastLastHeaderEl.removeCls(me.lastHeaderCls); + } + lastHeaderEl.addCls(me.lastHeaderCls); + me.pastLastHeaderEl = lastHeaderEl; + } + } + } + + }, + + onHeaderShow: function(header, preventLayout) { + // Pass up to the GridSection + var me = this, + gridSection = me.ownerCt, + menu = me.getMenu(), + topItems, topItemsVisible, + colCheckItem, + itemToEnable, + len, i; + + if (menu) { + + colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']'); + if (colCheckItem) { + colCheckItem.setChecked(true, true); + } + + // There's more than one header visible, and we've disabled some checked items... re-enable them + topItems = menu.query('#columnItem>menucheckitem[checked]'); + topItemsVisible = topItems.length; + if ((me.getVisibleGridColumns().length > 1) && me.disabledMenuItems && me.disabledMenuItems.length) { + if (topItemsVisible == 1) { + Ext.Array.remove(me.disabledMenuItems, topItems[0]); + } + for (i = 0, len = me.disabledMenuItems.length; i < len; i++) { + itemToEnable = me.disabledMenuItems[i]; + if (!itemToEnable.isDestroyed) { + itemToEnable[itemToEnable.menu ? 'enableCheckChange' : 'enable'](); + } + } + if (topItemsVisible == 1) { + me.disabledMenuItems = topItems; + } else { + me.disabledMenuItems = []; + } + } + } + + // Only update the grid UI when we are notified about base level Header shows; + // Group header shows just cause a layout of the HeaderContainer + if (!header.isGroupHeader) { + if (me.view) { + me.view.onHeaderShow(me, header, true); + } + if (gridSection) { + gridSection.onHeaderShow(me, header); + } + } + me.fireEvent('columnshow', me, header); + + // The header's own hide suppresses cascading layouts, so lay the headers out now + if (preventLayout !== true) { + me.doLayout(); + } + }, + + doComponentLayout: function(){ + var me = this; + if (me.view && me.view.saveScrollState) { + me.view.saveScrollState(); + } + me.callParent(arguments); + if (me.view && me.view.restoreScrollState) { + me.view.restoreScrollState(); + } + }, + + onHeaderHide: function(header, suppressLayout) { + // Pass up to the GridSection + var me = this, + gridSection = me.ownerCt, + menu = me.getMenu(), + colCheckItem; + + if (menu) { + + // If the header was hidden programmatically, sync the Menu state + colCheckItem = menu.down('menucheckitem[headerId=' + header.id + ']'); + if (colCheckItem) { + colCheckItem.setChecked(false, true); + } + me.setDisabledItems(); + } + + // Only update the UI when we are notified about base level Header hides; + if (!header.isGroupHeader) { + if (me.view) { + me.view.onHeaderHide(me, header, true); + } + if (gridSection) { + gridSection.onHeaderHide(me, header); + } + + // The header's own hide suppresses cascading layouts, so lay the headers out now + if (!suppressLayout) { + me.doLayout(); + } + } + me.fireEvent('columnhide', me, header); + }, + + setDisabledItems: function(){ + var me = this, + menu = me.getMenu(), + i = 0, + len, + itemsToDisable, + itemToDisable; + + // Find what to disable. If only one top level item remaining checked, we have to disable stuff. + itemsToDisable = menu.query('#columnItem>menucheckitem[checked]'); + if ((itemsToDisable.length === 1)) { + if (!me.disabledMenuItems) { + me.disabledMenuItems = []; + } + + // If down to only one column visible, also disable any descendant checkitems + if ((me.getVisibleGridColumns().length === 1) && itemsToDisable[0].menu) { + itemsToDisable = itemsToDisable.concat(itemsToDisable[0].menu.query('menucheckitem[checked]')); + } + + len = itemsToDisable.length; + // Disable any further unchecking at any level. + for (i = 0; i < len; i++) { + itemToDisable = itemsToDisable[i]; + if (!Ext.Array.contains(me.disabledMenuItems, itemToDisable)) { + + // If we only want to disable check change: it might be a disabled item, so enable it prior to + // setting its correct disablement level. + itemToDisable.disabled = false; + itemToDisable[itemToDisable.menu ? 'disableCheckChange' : 'disable'](); + me.disabledMenuItems.push(itemToDisable); + } + } + } + }, + + /** + * Temporarily lock the headerCt. This makes it so that clicking on headers + * don't trigger actions like sorting or opening of the header menu. This is + * done because extraneous events may be fired on the headers after interacting + * with a drag drop operation. + * @private + */ + tempLock: function() { + this.ddLock = true; + Ext.Function.defer(function() { + this.ddLock = false; + }, 200, this); + }, + + onHeaderResize: function(header, w, suppressFocus) { + this.tempLock(); + if (this.view && this.view.rendered) { + this.view.onHeaderResize(header, w, suppressFocus); + } + }, + + onHeaderClick: function(header, e, t) { + this.fireEvent("headerclick", this, header, e, t); + }, + + onHeaderTriggerClick: function(header, e, t) { + // generate and cache menu, provide ability to cancel/etc + if (this.fireEvent("headertriggerclick", this, header, e, t) !== false) { + this.showMenuBy(t, header); + } + }, + + showMenuBy: function(t, header) { + var menu = this.getMenu(), + ascItem = menu.down('#ascItem'), + descItem = menu.down('#descItem'), + sortableMth; + + menu.activeHeader = menu.ownerCt = header; + menu.setFloatParent(header); + // TODO: remove coupling to Header's titleContainer el + header.titleContainer.addCls(this.headerOpenCls); + + // enable or disable asc & desc menu items based on header being sortable + sortableMth = header.sortable ? 'enable' : 'disable'; + if (ascItem) { + ascItem[sortableMth](); + } + if (descItem) { + descItem[sortableMth](); + } + menu.showBy(t); + }, + + // remove the trigger open class when the menu is hidden + onMenuDeactivate: function() { + var menu = this.getMenu(); + // TODO: remove coupling to Header's titleContainer el + menu.activeHeader.titleContainer.removeCls(this.headerOpenCls); + }, + + moveHeader: function(fromIdx, toIdx) { + + // An automatically expiring lock + this.tempLock(); + this.onHeaderMoved(this.move(fromIdx, toIdx), fromIdx, toIdx); + }, + + purgeCache: function() { + var me = this; + // Delete column cache - column order has changed. + delete me.gridDataColumns; + delete me.hideableColumns; + + // Menu changes when columns are moved. It will be recreated. + if (me.menu) { + me.menu.destroy(); + delete me.menu; + } + }, + + onHeaderMoved: function(header, fromIdx, toIdx) { + var me = this, + gridSection = me.ownerCt; + + if (gridSection && gridSection.onHeaderMove) { + gridSection.onHeaderMove(me, header, fromIdx, toIdx); + } + me.fireEvent("columnmove", me, header, fromIdx, toIdx); + }, + + /** + * Gets the menu (and will create it if it doesn't already exist) + * @private + */ + getMenu: function() { + var me = this; + + if (!me.menu) { + me.menu = Ext.create('Ext.menu.Menu', { + hideOnParentHide: false, // Persists when owning ColumnHeader is hidden + items: me.getMenuItems(), + listeners: { + deactivate: me.onMenuDeactivate, + scope: me + } + }); + me.setDisabledItems(); + me.fireEvent('menucreate', me, me.menu); + } + return me.menu; + }, + + /** + * Returns an array of menu items to be placed into the shared menu + * across all headers in this header container. + * @returns {Array} menuItems + */ + getMenuItems: function() { + var me = this, + menuItems = [], + hideableColumns = me.enableColumnHide ? me.getColumnMenu(me) : null; + + if (me.sortable) { + menuItems = [{ + itemId: 'ascItem', + text: me.sortAscText, + cls: Ext.baseCSSPrefix + 'hmenu-sort-asc', + handler: me.onSortAscClick, + scope: me + },{ + itemId: 'descItem', + text: me.sortDescText, + cls: Ext.baseCSSPrefix + 'hmenu-sort-desc', + handler: me.onSortDescClick, + scope: me + }]; + } + if (hideableColumns && hideableColumns.length) { + menuItems.push('-', { + itemId: 'columnItem', + text: me.columnsText, + cls: Ext.baseCSSPrefix + 'cols-icon', + menu: hideableColumns + }); + } + return menuItems; + }, + + // sort asc when clicking on item in menu + onSortAscClick: function() { + var menu = this.getMenu(), + activeHeader = menu.activeHeader; + + activeHeader.setSortState('ASC'); + }, + + // sort desc when clicking on item in menu + onSortDescClick: function() { + var menu = this.getMenu(), + activeHeader = menu.activeHeader; + + activeHeader.setSortState('DESC'); + }, + + /** + * Returns an array of menu CheckItems corresponding to all immediate children of the passed Container which have been configured as hideable. + */ + getColumnMenu: function(headerContainer) { + var menuItems = [], i = 0, - it = this.items.items, - ln = it.length; + item, + items = headerContainer.query('>gridcolumn[hideable]'), + itemsLn = items.length, + menuItem; - el = Ext.getDom(el); - for (; i < ln; i++) { - item = it[i]; - itemEl = item.getEl(); - if ((itemEl.dom === el) || itemEl.contains(el)) { - return item; + for (; i < itemsLn; i++) { + item = items[i]; + menuItem = Ext.create('Ext.menu.CheckItem', { + text: item.text, + checked: !item.hidden, + hideOnClick: false, + headerId: item.id, + menu: item.isGroupHeader ? this.getColumnMenu(item) : undefined, + checkHandler: this.onColumnCheckChange, + scope: this + }); + if (itemsLn === 1) { + menuItem.disabled = true; } + menuItems.push(menuItem); + + // If the header is ever destroyed - for instance by dragging out the last remaining sub header, + // then the associated menu item must also be destroyed. + item.on({ + destroy: Ext.Function.bind(menuItem.destroy, menuItem) + }); + } + return menuItems; + }, + + onColumnCheckChange: function(checkItem, checked) { + var header = Ext.getCmp(checkItem.headerId); + header[checked ? 'show' : 'hide'](); + }, + + /** + * Get the columns used for generating a template via TableChunker. + * Returns an array of all columns and their + * - dataIndex + * - align + * - width + * - id + * - columnId - used to create an identifying CSS class + * - cls The tdCls configuration from the Column object + * @private + */ + getColumnsForTpl: function(flushCache) { + var cols = [], + headers = this.getGridColumns(flushCache), + headersLn = headers.length, + i = 0, + header, + width; + + for (; i < headersLn; i++) { + header = headers[i]; + + if (header.hidden || header.up('headercontainer[hidden=true]')) { + width = 0; + } else { + width = header.getDesiredWidth(); + // IE6 and IE7 bug. + // Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy. + // We need to increment the passed with in this case. + if ((i === 0) && (Ext.isIE6 || Ext.isIE7)) { + width += 1; + } + } + cols.push({ + dataIndex: header.dataIndex, + align: header.align, + width: width, + id: header.id, + cls: header.tdCls, + columnId: header.getItemId() + }); + } + return cols; + }, + + /** + * Returns the number of <b>grid columns</b> descended from this HeaderContainer. + * Group Columns are HeaderContainers. All grid columns are returned, including hidden ones. + */ + getColumnCount: function() { + return this.getGridColumns().length; + }, + + /** + * Gets the full width of all columns that are visible. + */ + getFullWidth: function(flushCache) { + var fullWidth = 0, + headers = this.getVisibleGridColumns(flushCache), + headersLn = headers.length, + i = 0; + + for (; i < headersLn; i++) { + if (!isNaN(headers[i].width)) { + // use headers getDesiredWidth if its there + if (headers[i].getDesiredWidth) { + fullWidth += headers[i].getDesiredWidth(); + // if injected a diff cmp use getWidth + } else { + fullWidth += headers[i].getWidth(); + } + } + } + return fullWidth; + }, + + // invoked internally by a header when not using triStateSorting + clearOtherSortStates: function(activeHeader) { + var headers = this.getGridColumns(), + headersLn = headers.length, + i = 0, + oldSortState; + + for (; i < headersLn; i++) { + if (headers[i] !== activeHeader) { + oldSortState = headers[i].sortState; + // unset the sortstate and dont recurse + headers[i].setSortState(null, true); + //if (!silent && oldSortState !== null) { + // this.fireEvent('sortchange', this, headers[i], null); + //} + } + } + }, + + /** + * Returns an array of the <b>visible</b> columns in the grid. This goes down to the lowest column header + * level, and does not return <i>grouped</i> headers which contain sub headers. + * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache. + * @returns {Array} + */ + getVisibleGridColumns: function(refreshCache) { + return Ext.ComponentQuery.query(':not([hidden])', this.getGridColumns(refreshCache)); + }, + + /** + * Returns an array of all columns which map to Store fields. This goes down to the lowest column header + * level, and does not return <i>grouped</i> headers which contain sub headers. + * @param {Boolean} refreshCache If omitted, the cached set of columns will be returned. Pass true to refresh the cache. + * @returns {Array} + */ + getGridColumns: function(refreshCache) { + var me = this, + result = refreshCache ? null : me.gridDataColumns; + + // Not already got the column cache, so collect the base columns + if (!result) { + me.gridDataColumns = result = []; + me.cascade(function(c) { + if ((c !== me) && !c.isGroupHeader) { + result.push(c); + } + }); + } + + return result; + }, + + /** + * @private + * For use by column headers in determining whether there are any hideable columns when deciding whether or not + * the header menu should be disabled. + */ + getHideableColumns: function(refreshCache) { + var me = this, + result = refreshCache ? null : me.hideableColumns; + + if (!result) { + result = me.hideableColumns = me.query('[hideable]'); + } + return result; + }, + + /** + * Get the index of a leaf level header regardless of what the nesting + * structure is. + */ + getHeaderIndex: function(header) { + var columns = this.getGridColumns(); + return Ext.Array.indexOf(columns, header); + }, + + /** + * Get a leaf level header by index regardless of what the nesting + * structure is. + */ + getHeaderAtIndex: function(index) { + var columns = this.getGridColumns(); + return columns[index]; + }, + + /** + * Maps the record data to base it on the header id's. + * This correlates to the markup/template generated by + * TableChunker. + */ + prepareData: function(data, rowIdx, record, view, panel) { + var obj = {}, + headers = this.gridDataColumns || this.getGridColumns(), + headersLn = headers.length, + colIdx = 0, + header, + headerId, + renderer, + value, + metaData, + store = panel.store; + + for (; colIdx < headersLn; colIdx++) { + metaData = { + tdCls: '', + style: '' + }; + header = headers[colIdx]; + headerId = header.id; + renderer = header.renderer; + value = data[header.dataIndex]; + + // When specifying a renderer as a string, it always resolves + // to Ext.util.Format + if (typeof renderer === "string") { + header.renderer = renderer = Ext.util.Format[renderer]; + } + + if (typeof renderer === "function") { + value = renderer.call( + header.scope || this.ownerCt, + value, + // metadata per cell passing an obj by reference so that + // it can be manipulated inside the renderer + metaData, + record, + rowIdx, + colIdx, + store, + view + ); + } + + // <debug> + if (metaData.css) { + // This warning attribute is used by the compat layer + obj.cssWarning = true; + metaData.tdCls = metaData.css; + delete metaData.css; + } + // </debug> + + obj[headerId+'-modified'] = record.isModified(header.dataIndex) ? Ext.baseCSSPrefix + 'grid-dirty-cell' : ''; + obj[headerId+'-tdCls'] = metaData.tdCls; + obj[headerId+'-tdAttr'] = metaData.tdAttr; + obj[headerId+'-style'] = metaData.style; + if (value === undefined || value === null || value === '') { + value = ' '; + } + obj[headerId] = value; + } + return obj; + }, + + expandToFit: function(header) { + if (this.view) { + this.view.expandToFit(header); } - return null; } });