X-Git-Url: http://git.ithinksw.org/extjs.git/blobdiff_plain/0494b8d9b9bb03ab6c22b34dae81261e3cd7e3e6..7a654f8d43fdb43d78b63d90528bed6e86b608cc:/src/grid/header/Container.js diff --git a/src/grid/header/Container.js b/src/grid/header/Container.js new file mode 100644 index 00000000..4c8e468c --- /dev/null +++ b/src/grid/header/Container.js @@ -0,0 +1,762 @@ +/** + * @class Ext.grid.header.Container + * @extends Ext.container.Container + * @private + * + * 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. + * + * @xtype headercontainer + */ +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, + + /** + * true 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(); + }, + + // 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 = 'h' + (++me.headerCounter); + } + 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; + + me.callParent(arguments); + + if (topHeaders.length) { + topHeaders[0].el.radioCls(me.firstHeaderCls); + topHeaders[topHeaders.length - 1].el.radioCls(me.lastHeaderCls); + } + } + }, + + onHeaderShow: function(header) { + // 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 + me.doLayout(); + }, + + 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)) { + 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); + } + this.fireEvent('columnresize', this, header, w); + }, + + 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; + + // 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(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', { + 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 = [{ + itemId: 'columnItem', + text: me.columnsText, + cls: Ext.baseCSSPrefix + 'cols-icon', + menu: me.getColumnMenu(me) + }]; + + if (me.sortable) { + menuItems.unshift({ + itemId: 'ascItem', + text: me.sortAscText, + cls: 'xg-hmenu-sort-asc', + handler: me.onSortAscClick, + scope: me + },{ + itemId: 'descItem', + text: me.sortDescText, + cls: 'xg-hmenu-sort-desc', + handler: me.onSortDescClick, + scope: me + },'-'); + } + 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, + item, + items = headerContainer.query('>gridcolumn[hideable]'), + itemsLn = items.length, + menuItem; + + 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; + + for (; i < headersLn; i++) { + header = headers[i]; + cols.push({ + dataIndex: header.dataIndex, + align: header.align, + width: header.hidden ? 0 : header.getDesiredWidth(), + id: header.id, + cls: header.tdCls, + columnId: header.getItemId() + }); + } + return cols; + }, + + /** + * Returns the number of grid columns 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 visible columns in the grid. This goes down to the lowest column header + * level, and does not return grouped 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 grouped 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; + }, + + /** + * 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) { + var obj = {}, + headers = this.getGridColumns(), + headersLn = headers.length, + colIdx = 0, + header, value, + metaData, + g = this.up('tablepanel'), + store = g.store; + + for (; colIdx < headersLn; colIdx++) { + metaData = { + tdCls: '', + style: '' + }; + header = headers[colIdx]; + value = data[header.dataIndex]; + + // When specifying a renderer as a string, it always resolves + // to Ext.util.Format + if (Ext.isString(header.renderer)) { + header.renderer = Ext.util.Format[header.renderer]; + } + + if (Ext.isFunction(header.renderer)) { + value = header.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 + ); + } + + // + if (metaData.css) { + // This warning attribute is used by the compat layer + obj.cssWarning = true; + metaData.tdCls = metaData.css; + delete metaData.css; + } + // + obj[header.id+'-modified'] = record.isModified(header.dataIndex) ? Ext.baseCSSPrefix + 'grid-dirty-cell' : Ext.baseCSSPrefix + 'grid-clean-cell'; + obj[header.id+'-tdCls'] = metaData.tdCls; + obj[header.id+'-tdAttr'] = metaData.tdAttr; + obj[header.id+'-style'] = metaData.style; + if (value === undefined || value === null || value === '') { + value = ' '; + } + obj[header.id] = value; + } + return obj; + }, + + expandToFit: function(header) { + if (this.view) { + this.view.expandToFit(header); + } + } +});