2 * Headercontainer is a docked container (_`top` or `bottom` only_) that holds the
3 * headers ({@link Ext.grid.column.Column grid columns}) of a
4 * {@link Ext.grid.Panel grid} or {@link Ext.tree.Panel tree}. The headercontainer
5 * handles resizing, moving, and hiding columns. As columns are hidden, moved or
6 * resized, the headercontainer triggers changes within the grid or tree's
7 * {@link Ext.view.Table view}. You will not generally need to instantiate this class
11 * {@link Ext.panel.Table#method-getHeaderContainer getHeaderContainer()}
12 * accessor method to access the tree or grid's headercontainer.
14 * Grids and trees also have an alias to the two more useful headercontainer methods:
16 * - **{@link Ext.panel.Table#method-getColumns getColumns}** - aliases
17 * {@link Ext.grid.header.Container#getGridColumns}
18 * - **{@link Ext.panel.Table#method-getVisibleColumns getVisibleColumns}** - aliases
19 * {@link Ext.grid.header.Container#getVisibleGridColumns}
21 Ext
.define('Ext.grid.header.Container', {
22 extend
: 'Ext.container.Container',
24 'Ext.grid.ColumnLayout',
25 'Ext.grid.plugin.HeaderResizer',
26 'Ext.grid.plugin.HeaderReorderer',
30 'Ext.grid.column.Column',
31 'Ext.grid.ColumnManager',
38 'Ext.util.FocusableContainer'
43 alias
: 'widget.headercontainer',
45 baseCls
: Ext
.baseCSSPrefix
+ 'grid-header-ct',
50 * @cfg {Number} weight
51 * HeaderContainer overrides the default weight of 0 for all docked items to 100.
52 * This is so that it has more priority over things like toolbars.
56 defaultType
: 'gridcolumn',
58 detachOnRemove
: false,
61 * @cfg {Number} defaultWidth
62 * Width of the header if no width or flex is specified.
67 * @cfg {Boolean} [sealed=false]
68 * Specify as `true` to constrain column dragging so that a column cannot be dragged into or out of this column.
70 * **Note that this config is only valid for column headers which contain child column headers, eg:**
86 sortAscText
: 'Sort Ascending',
89 sortDescText
: 'Sort Descending',
92 sortClearText
: 'Clear Sort',
95 columnsText
: 'Columns',
98 headerOpenCls
: Ext
.baseCSSPrefix
+ 'column-header-open',
100 menuSortAscCls
: Ext
.baseCSSPrefix
+ 'hmenu-sort-asc',
102 menuSortDescCls
: Ext
.baseCSSPrefix
+ 'hmenu-sort-desc',
104 menuColsIcon
: Ext
.baseCSSPrefix
+ 'cols-icon',
110 // May be set to false by a SptreadSheetSelectionModel
113 // Disable FocusableContainer behavior by default, since we only want it
114 // to be enabled for the root header container (we'll set the flag in initComponent)
115 enableFocusableContainer
: false,
120 * @property {Boolean} isGroupHeader
121 * True if this HeaderContainer is in fact a group header which contains sub headers.
125 * @cfg {Boolean} sortable
126 * Provides the default sortable state for all Headers within this HeaderContainer.
127 * Also turns on or off the menus in the HeaderContainer. Note that the menu is
128 * shared across every header and therefore turning it off will remove the menu
129 * items for every header.
134 * @cfg {Boolean} [enableColumnHide=true]
135 * False to disable column hiding within this grid.
137 enableColumnHide
: true,
140 * @event columnresize
141 * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
142 * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
143 * @param {Number} width
148 * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
149 * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
150 * @param {Ext.event.Event} e
151 * @param {HTMLElement} t
155 * @event headercontextmenu
156 * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
157 * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
158 * @param {Ext.event.Event} e
159 * @param {HTMLElement} t
163 * @event headertriggerclick
164 * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
165 * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
166 * @param {Ext.event.Event} e
167 * @param {HTMLElement} t
172 * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
173 * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
174 * @param {Number} fromIdx
175 * @param {Number} toIdx
180 * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
181 * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
186 * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
187 * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
191 * @event columnschanged
192 * Fired after the columns change in any way, when a column has been hidden or shown, or when a column
193 * is added to or removed from this header container.
194 * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
199 * @param {Ext.grid.header.Container} ct The grid's header Container which encapsulates all column headers.
200 * @param {Ext.grid.column.Column} column The Column header Component which provides the column definition
201 * @param {String} direction
206 * Fired immediately after the column header menu is created.
207 * @param {Ext.grid.header.Container} ct This instance
208 * @param {Ext.menu.Menu} menu The Menu that was created
212 * @event headermenucreate
213 * Fired immediately after the column header menu is created.
214 * @param {Ext.panel.Table} grid This grid instance
215 * @param {Ext.menu.Menu} menu The Menu that was created
216 * @param {Ext.grid.header.Container} headerCt This header container
217 * @member Ext.panel.Table
220 initComponent: function() {
223 me
.plugins
= me
.plugins
|| [];
224 me
.defaults
= me
.defaults
|| {};
226 // TODO: Pass in configurations to turn on/off dynamic
227 // resizing and disable resizing all together
229 // Only set up a Resizer and Reorderer for the topmost HeaderContainer.
230 // Nested Group Headers are themselves HeaderContainers
232 if (me
.enableColumnResize
) {
233 me
.resizer
= new Ext
.grid
.plugin
.HeaderResizer();
234 me
.plugins
.push(me
.resizer
);
236 if (me
.enableColumnMove
) {
237 me
.reorderer
= new Ext
.grid
.plugin
.HeaderReorderer();
238 me
.plugins
.push(me
.reorderer
);
242 // If this is a leaf column header, and is NOT functioning as a container,
243 // use Container layout with a no-op calculate method.
244 if (me
.isColumn
&& !me
.isGroupHeader
) {
245 if (!me
.items
|| me
.items
.length
=== 0) {
246 me
.isContainer
= me
.isFocusableContainer
= false;
250 calculate
: Ext
.emptyFn
254 // HeaderContainer and Group header needs a gridcolumn layout.
256 me
.layout
= Ext
.apply({
259 }, me
.initialConfig
.layout
);
261 // All HeaderContainers need to know this so that leaf Columns can adjust for cell border width if using content box model
262 me
.defaults
.columnLines
= me
.columnLines
;
264 // If the header isn't a column ([isColumn] or [isGroupHeader]), then it's the root header.
265 if (!me
.isGroupHeader
) {
266 me
.isRootHeader
= true;
268 // The root header is a focusableContainer if it's not carrying hidden headers.
269 if (!me
.hiddenHeaders
) {
270 me
.enableFocusableContainer
= true;
271 me
.ariaRole
= 'rowgroup';
274 // Create column managers for the root header.
275 me
.columnManager
= new Ext
.grid
.ColumnManager(false, me
);
276 me
.visibleColumnManager
= new Ext
.grid
.ColumnManager(true, me
);
278 // In the grid config, if grid.columns is a header container instance and not a columns
279 // config, then it currently has no knowledge of a containing grid. Create the column
280 // manager now and bind it to the grid later in Ext.panel.Table:initComponent().
282 // In most cases, though, grid.columns will be a config, so the grid is already known
283 // and the column manager can be bound to it.
285 me
.grid
.columnManager
= me
.columnManager
;
286 me
.grid
.visibleColumnManager
= me
.visibleColumnManager
;
289 // Is a group header, also create column managers.
290 me
.visibleColumnManager
= new Ext
.grid
.ColumnManager(true, me
);
291 me
.columnManager
= new Ext
.grid
.ColumnManager(false, me
);
295 me
.menuTask
= new Ext
.util
.DelayedTask(me
.updateMenuDisabledState
, me
);
299 insertNestedHeader: function (moveHeader
) {
301 fromCt
= moveHeader
.ownerCt
,
303 layoutOwner
= toCt
.layout
.owner
,
307 if (me
.isGroupHeader
&& !toCt
.isNestedParent
) {
308 toIndex
= layoutOwner
.items
.indexOf(me
);
311 fromCt
.remove(moveHeader
, false);
314 if (toIndex
=== undefined) {
315 toIndex
= layoutOwner
.items
.indexOf(me
);
318 layoutOwner
.insert(toIndex
, moveHeader
);
321 isNested: function () {
322 return !!this.getRootHeaderCt().down('[isNestedParent]');
325 isNestedGroupHeader: function () {
326 // The owner only has one item that isn't hidden and it's me; hide the owner.
328 items
= header
.getRefOwner().query('>:not([hidden])');
330 return (items
.length
=== 1 && items
[0] === header
);
333 maybeShowNestedGroupHeader: function () {
334 // Group headers are special in that they are auto-hidden when their subheaders are all
335 // hidden and auto-shown when the first subheader is reshown. They are the only headers
336 // that should now be auto-shown or -hidden.
338 // It follows that since group headers are dictated by some automation depending upon the
339 // state of their child items that all group headers should be shown if anyone in the
340 // hierarchy is shown since these special group headers only contain one child, which is
341 // the next group header in the stack.
342 // This only should apply to the following grouped header scenario:
344 // +-----------------------------------+
346 // |-----------------------------------|
348 // other |-----------------------------------| other
349 // headers | Group 3 | headers
350 // |-----------------------------------|
351 // | Field3 | Field4 | Field5 | Field6 |
352 // |===================================|
354 // +-----------------------------------+
356 var items
= this.items
,
359 if (items
&& items
.length
=== 1 && (item
= items
.getAt(0)) && item
.hidden
) {
364 setNestedParent: function (target
) {
365 // Here we need to prevent the removal of ancestor group headers from occuring if a flag is set. This
366 // is needed when there are stacked group headers and only the deepest nested group header has leaf items
367 // in its collection. In this specific scenario, the group headers above it only have 1 item, which is its
368 // child nested group header.
370 // If we don't set this flag, then all of the grouped headers will be recursively removed all the way up to
371 // the root container b/c Ext.grid.header.Container#onRemove will remove all containers that don't contain
374 // Note that if an ownerCt only has one item, then we know that this item is the group header that we're
375 // currently dragging.
377 // Also, note that we mark the owner as the target header because everything up to that should be removed.
379 // We have to reset any previous headers that may have been target.ownerCts!
380 target
.isNestedParent
= false;
381 target
.ownerCt
.isNestedParent
= !!(this.ownerCt
.items
.length
=== 1 && target
.ownerCt
.items
.length
=== 1);
384 initEvents: function() {
391 // If this is top level, listen for events to delegate to descendant headers.
392 if (!me
.isColumn
&& !me
.isGroupHeader
) {
393 onHeaderCtEvent
= me
.onHeaderCtEvent
;
395 click
: onHeaderCtEvent
,
396 dblclick
: onHeaderCtEvent
,
397 contextmenu
: onHeaderCtEvent
,
398 mouseover
: me
.onHeaderCtMouseOver
,
399 mouseout
: me
.onHeaderCtMouseOut
,
403 if (Ext
.supports
.Touch
) {
404 listeners
.longpress
= me
.onHeaderCtLongPress
;
406 me
.mon(me
.el
, listeners
);
410 onHeaderCtEvent: function(e
, t
) {
412 headerEl
= me
.getHeaderElByEvent(e
),
417 if (me
.longPressFired
) {
418 // if we just showed the menu as a result of a longpress, do not process
419 // the click event and sort the column.
420 me
.longPressFired
= false;
424 if (headerEl
&& !me
.blockEvents
) {
425 header
= Ext
.getCmp(headerEl
.id
);
427 targetEl
= header
[header
.clickTargetName
];
428 // If there's no possibility that the mouseEvent was on child header items,
429 // or it was definitely in our titleEl, then process it
430 if ((!header
.isGroupHeader
&& !header
.isContainer
) || e
.within(targetEl
)) {
431 if (e
.type
=== 'click' || e
.type
=== 'tap') {
432 // The header decides which header to activate on click
433 // on Touch, anywhere in the splitter zone activates
435 activeHeader
= header
.onTitleElClick(e
, targetEl
, me
.sortOnClick
);
437 // If activated by touch, there is no trigger el to align with, so align to the header element.
438 me
.onHeaderTriggerClick(activeHeader
, e
, e
.pointerType
=== 'touch' ? activeHeader
.el
: activeHeader
.triggerEl
);
440 me
.onHeaderClick(header
, e
, t
);
443 else if (e
.type
=== 'contextmenu') {
444 me
.onHeaderContextMenu(header
, e
, t
);
445 } else if (e
.type
=== 'dblclick' && header
.resizable
) {
446 header
.onTitleElDblClick(e
, targetEl
.dom
);
453 blockNextEvent: function() {
454 this.blockEvents
= true;
455 Ext
.asap(this.unblockEvents
, this);
458 unblockEvents: function() {
459 this.blockEvents
= false;
462 onHeaderCtMouseOver: function(e
, t
) {
467 // Only proces the mouse entering this HeaderContainer.
468 // From header to header, and exiting this HeaderContainer we track using mouseout events.
469 if (!e
.within(this.el
, true)) {
470 headerEl
= e
.getTarget('.' + Ext
.grid
.column
.Column
.prototype.baseCls
);
471 header
= headerEl
&& Ext
.getCmp(headerEl
.id
);
473 targetEl
= header
[header
.clickTargetName
];
474 if (e
.within(targetEl
)) {
475 header
.onTitleMouseOver(e
, targetEl
.dom
);
481 onHeaderCtMouseOut: function(e
, t
) {
482 var headerSelector
= '.' + Ext
.grid
.column
.Column
.prototype.baseCls
,
483 outHeaderEl
= e
.getTarget(headerSelector
),
484 inHeaderEl
= e
.getRelatedTarget(headerSelector
),
488 // It's a mouseenter/leave, not an internal element change within a Header
489 if (outHeaderEl
!== inHeaderEl
) {
491 header
= Ext
.getCmp(outHeaderEl
.id
);
493 targetEl
= header
[header
.clickTargetName
];
494 header
.onTitleMouseOut(e
, targetEl
.dom
);
498 header
= Ext
.getCmp(inHeaderEl
.id
);
500 targetEl
= header
[header
.clickTargetName
];
501 header
.onTitleMouseOver(e
, targetEl
.dom
);
507 onHeaderCtLongPress: function(e
) {
509 headerEl
= me
.getHeaderElByEvent(e
),
510 header
= Ext
.getCmp(headerEl
.id
);
512 if (!header
.menuDisabled
) {
513 me
.longPressFired
= true;
514 me
.showMenuBy(e
, headerEl
, header
);
518 getHeaderElByEvent: function(e
) {
519 return e
.getTarget('.' + Ext
.grid
.column
.Column
.prototype.baseCls
);
522 isLayoutRoot: function(){
523 // Since we're docked, the width is always calculated
524 // If we're hidden, the height is explicitly 0, which
525 // means we'll be considered a layout root. However, we
526 // still need the view to layout to update the underlying
527 // table to match the size.
528 if (this.hiddenHeaders
) {
531 return this.callParent();
534 // Find the topmost HeaderContainer
535 getRootHeaderCt: function() {
537 return me
.isRootHeader
? me
: me
.up('[isRootHeader]');
540 onDestroy: function() {
544 me
.menu
.un('hide', me
.onMenuHide
, me
);
546 me
.menuTask
.cancel();
548 Ext
.destroy(me
.visibleColumnManager
, me
.columnManager
, me
.menu
);
549 me
.columnManager
= me
.visibleColumnManager
= null;
552 applyColumnsState: function(columns
, storeState
) {
553 if (!columns
|| !columns
.length
) {
558 items
= me
.items
.items
,
559 count
= items
.length
,
561 length
= columns
.length
,
562 c
, col
, columnState
, index
,
568 // Create state lookup hash
579 for (c
= 0; c
< length
; c
++) {
580 columnState
= columns
[c
];
581 columnState
.index
= c
;
582 stateHash
[columnState
.id
] = columnState
;
585 for (i
= 0; i
< count
; i
++) {
587 columnState
= stateHash
[col
.getStateId()];
589 // There's a column state for this column.
590 // Add it to the newOrder array at the specified index
592 index
= columnState
.index
;
593 newOrder
[index
] = col
;
598 if (col
.applyColumnState
) {
599 col
.applyColumnState(columnState
, storeState
);
603 // It must be inserted at this index after state restoration,
612 // If any saved columns were missing, close the gaps where they were
613 newOrder
= Ext
.Array
.clean(newOrder
);
615 // New column encountered.
616 // Insert them into the newOrder at their configured position
617 length
= newCols
.length
;
619 for (i
= 0; i
< length
; i
++) {
620 columnState
= newCols
[i
];
621 index
= columnState
.index
;
622 if (index
< newOrder
.length
) {
624 Ext
.Array
.splice(newOrder
, index
, 0, columnState
.column
);
626 newOrder
.push(columnState
.column
);
632 // This flag will prevent the groupheader from being removed by its owner when it (temporarily) has no child items.
633 me
.applyingState
= true;
635 delete me
.applyingState
;
642 getColumnsState: function () {
647 me
.items
.each(function (col
) {
648 state
= col
.getColumnState
&& col
.getColumnState();
657 // Invalidate column cache on add
658 // We cannot refresh the View on every add because this method is called
659 // when the HeaderDropZone moves Headers around, that will also refresh the view
667 if (me
._usedIDs
[c
.headerId
]) {
668 Ext
.log
.warn(this.$className
+ ' attempted to reuse an existing id: ' + c
.headerId
);
670 me
._usedIDs
[c
.headerId
] = true;
673 me
.callParent(arguments
);
675 me
.onHeadersChanged(c
, me
.isDDMoveInGrid
);
678 move: function(fromIdx
, toIdx
) {
683 if (fromIdx
.isComponent
) {
684 headerToMove
= fromIdx
;
685 fromIdx
= items
.indexOf(headerToMove
);
687 headerToMove
= items
.getAt(fromIdx
);
690 // Take real grid column index of column being moved
691 headerToMove
.visibleFromIdx
= me
.getRootHeaderCt().visibleColumnManager
.indexOf(headerToMove
);
693 me
.callParent(arguments
);
696 onMove: function(headerToMove
, fromIdx
, toIdx
) {
698 gridHeaderCt
= me
.getRootHeaderCt(),
699 gridVisibleColumnManager
= gridHeaderCt
.visibleColumnManager
,
703 // Purges cache so that indexOf returns new position of header
704 me
.onHeadersChanged(headerToMove
, true);
706 visibleToIdx
= gridVisibleColumnManager
.indexOf(headerToMove
);
707 if (visibleToIdx
>= headerToMove
.visibleFromIdx
) {
711 me
.callParent(arguments
);
713 // If what is being moved is a group header, then pass the correct column count
714 if (headerToMove
.isGroupHeader
) {
715 numColsToMove
= headerToMove
.visibleColumnManager
.getColumns().length
;
718 gridHeaderCt
.onHeaderMoved(headerToMove
, numColsToMove
, headerToMove
.visibleFromIdx
, visibleToIdx
);
722 maybeContinueRemove: function () {
725 // Note that if the column is a group header and is the current target of a drag, we don't want to remove it
726 // if it since it could be one of any number of (empty) nested group headers.
729 // There are also other scenarios in which the remove should not occur. For instance, when applying column
730 // state to a groupheader, the subheaders are all removed before being re-added in their stateful order,
731 // and the groupheader should not be removed in the meantime.
733 return (me
.isGroupHeader
&& !me
.applyingState
) && !me
.isNestedParent
&& me
.ownerCt
&& !me
.items
.getCount();
736 // Invalidate column cache on remove
737 // We cannot refresh the View on every remove because this method is called
738 // when the HeaderDropZone moves Headers around, that will also refresh the view
739 onRemove: function(c
, isDestroying
) {
741 ownerCt
= me
.ownerCt
,
742 lastHiddenHeader
= c
.lastHiddenHeader
;
744 me
.callParent([c
, isDestroying
]);
750 delete me
._usedIDs
[c
.headerId
];
753 if (!me
.destroying
) {
754 // isDDMoveInGrid flag set by Ext.grid.header.DropZone when moving into another container *within the same grid*.
755 // This stops header change processing from being executed twice, once on remove and then on the subsequent add.
756 if (!me
.isDDMoveInGrid
) {
757 me
.onHeadersChanged(c
, false);
760 if (me
.maybeContinueRemove()) {
761 // Detach the header from the DOM here. Since we're removing and destroying the container,
762 // the inner DOM may get overwritten, since Container::deatchOnRemove gets processed after
765 me
.detachComponent(c
);
767 // If we don't have any items left and we're a group, remove ourselves.
768 // This will cascade up if necessary
769 Ext
.suspendLayouts();
771 Ext
.resumeLayouts(true);
777 // Called to clear all caches of columns whenever columns are added, removed to just moved.
778 // We need to be informed if it's just a move operation so that we don't call the heavier
779 // grid.onHeadersChanged which refreshes the view.
780 // The onMove handler ensures that grid.inHeaderMove is called which just swaps cells.
781 onHeadersChanged: function(c
, isMove
) {
783 gridHeaderCt
= this.getRootHeaderCt();
785 // Each HeaderContainer up the chain must have its cache purged so that its getGridColumns method will return correct results.
786 this.purgeHeaderCtCache(this);
789 gridHeaderCt
.onColumnsChanged();
790 if (!c
.isGroupHeader
) {
791 gridPanel
= gridHeaderCt
.ownerCt
;
793 // If it an add or remove operation causing this header change call, then inform the grid which refreshes.
794 // Moving calls the onHeaderMoved method of the grid which just swaps cells.
795 if (gridPanel
&& !isMove
) {
796 gridPanel
.onHeadersChanged(gridHeaderCt
, c
);
803 onHeaderMoved: function(header
, colsToMove
, fromIdx
, toIdx
) {
805 gridSection
= me
.ownerCt
;
808 if (gridSection
&& gridSection
.onHeaderMove
) {
809 gridSection
.onHeaderMove(me
, header
, colsToMove
, fromIdx
, toIdx
);
811 me
.fireEvent('columnmove', me
, header
, fromIdx
, toIdx
);
816 // Only called on the grid's headerCt.
817 // Called whenever a column is added or removed or moved at any level below.
818 // Ensures that the gridColumns caches are cleared.
819 onColumnsChanged: function() {
826 me
.fireEvent('columnschanged', me
);
828 // Column item (and its associated menu) menu has to be destroyed (if it exits) when columns are changed.
829 // It will be recreated just before the main container menu is next shown.
830 if (menu
&& (columnItemSeparator
= menu
.child('#columnItemSeparator'))) {
831 columnItem
= menu
.child('#columnItem');
833 // Destroy the column visibility items
834 // They will be recreated before the next show
835 columnItemSeparator
.destroy();
836 columnItem
.destroy();
844 lookupComponent: function(comp
) {
845 var result
= this.callParent(arguments
);
847 // Apply default width unless it's a group header (in which case it must be left to shrinkwrap), or it's flexed.
848 // Test whether width is undefined so that width: null can be used to have the header shrinkwrap its text.
849 if (!result
.isGroupHeader
&& result
.width
=== undefined && !result
.flex
) {
850 result
.width
= this.defaultWidth
;
857 * Synchronize column UI visible sort state with Store's sorters.
859 setSortState: function() {
860 var store
= this.up('[store]').store
,
861 columns
= this.visibleColumnManager
.getColumns(),
862 len
= columns
.length
, i
,
865 for (i
= 0; i
< len
; i
++) {
868 // Access the column's custom sorter in preference to one keyed on the data index.
869 sorter
= header
.getSorter();
871 // If the column was configured with a sorter, we must check that the sorter
872 // is part of the store's sorter collection to update the UI to the correct state.
873 // The store may not actually BE sorted by that sorter.
874 if (!store
.getSorters().contains(sorter
)) {
878 sorter
= store
.getSorters().get(header
.getSortParam());
881 // Important: A null sorter for this column will *clear* the UI sort indicator.
882 header
.setSortState(sorter
);
886 getHeaderMenu: function(){
887 var menu
= this.getMenu(),
891 item
= menu
.child('#columnItem');
899 onHeaderVisibilityChange: function(header
, visible
){
901 menu
= me
.getHeaderMenu(),
904 // Invalidate column collections upon column hide/show
905 me
.purgeHeaderCtCache(header
.ownerCt
);
908 // If the header was hidden programmatically, sync the Menu state
909 item
= me
.getMenuItemForHeader(menu
, header
);
911 item
.setChecked(visible
, true);
913 // delay this since the headers may fire a number of times if we're hiding/showing groups
914 if (menu
.isVisible()) {
915 me
.menuTask
.delay(50);
920 updateMenuDisabledState: function(menu
) {
922 columns
= me
.query('gridcolumn:not([hidden])'),
924 len
= columns
.length
,
929 // If called from menu creation, it will be passed to avoid infinite recursion
934 for (i
= 0; i
< len
; ++i
) {
936 checkItem
= me
.getMenuItemForHeader(menu
, item
);
938 method
= item
.isHideable() ? 'enable' : 'disable';
939 if (checkItem
.menu
) {
940 method
+= 'CheckChange';
947 getMenuItemForHeader: function(menu
, header
) {
948 return header
? menu
.down('menucheckitem[headerId=' + header
.id
+ ']') : null;
951 onHeaderShow: function (header
) {
953 ownerCt
= me
.ownerCt
,
954 lastHiddenHeader
= header
.lastHiddenHeader
;
964 // If lastHiddenHeader exists we know that header is a groupHeader and if all its subheaders
965 // are hidden then we need to show the last one that was hidden.
966 if (lastHiddenHeader
&& !header
.query('[hidden=false]').length
) {
967 lastHiddenHeader
.show();
968 header
.lastHiddenHeader
= null;
971 me
.onHeaderVisibilityChange(header
, true);
972 ownerCt
.onHeaderShow(me
, header
);
974 me
.fireEvent('columnshow', me
, header
);
975 me
.fireEvent('columnschanged', this);
978 onHeaderHide: function (header
) {
980 ownerCt
= me
.ownerCt
;
986 me
.onHeaderVisibilityChange(header
, false);
987 ownerCt
.onHeaderHide(me
, header
);
989 me
.fireEvent('columnhide', me
, header
);
990 me
.fireEvent('columnschanged', this);
993 onHeaderResize: function(header
, w
) {
995 gridSection
= me
.ownerCt
;
998 gridSection
.onHeaderResize(me
, header
, w
);
1000 me
.fireEvent('columnresize', me
, header
, w
);
1003 onHeaderClick: function(header
, e
, t
) {
1005 selModel
= header
.getView().getSelectionModel();
1007 header
.fireEvent('headerclick', me
, header
, e
, t
);
1008 if (me
.fireEvent('headerclick', me
, header
, e
, t
) !== false) {
1009 if (selModel
.onHeaderClick
) {
1010 selModel
.onHeaderClick(me
, header
, e
);
1015 onHeaderContextMenu: function(header
, e
, t
) {
1016 header
.fireEvent('headercontextmenu', this, header
, e
, t
);
1017 this.fireEvent('headercontextmenu', this, header
, e
, t
);
1020 onHeaderTriggerClick: function(header
, e
, t
) {
1022 if (header
.fireEvent('headertriggerclick', me
, header
, e
, t
) !== false && me
.fireEvent('headertriggerclick', me
, header
, e
, t
) !== false) {
1024 // If menu is already active...
1025 if (header
.activeMenu
) {
1026 // Click/tap toggles the menu visibility.
1027 if (e
.pointerType
) {
1028 header
.activeMenu
.hide();
1030 header
.activeMenu
.focus();
1034 me
.showMenuBy(e
, t
, header
);
1042 * Shows the column menu under the target element passed. This method is used when the trigger element on the column
1043 * header is clicked on and rarely should be used otherwise.
1045 * @param {Ext.event.Event} [event] The event which triggered the current handler. If omitted
1046 * or a key event, the menu autofocuses its first item.
1047 * @param {HTMLElement/Ext.dom.Element} t The target to show the menu by
1048 * @param {Ext.grid.header.Container} header The header container that the trigger was clicked on.
1050 showMenuBy: function(clickEvent
, t
, header
) {
1051 var menu
= this.getMenu(),
1052 ascItem
= menu
.down('#ascItem'),
1053 descItem
= menu
.down('#descItem'),
1055 isTouch
= clickEvent
&& clickEvent
.pointerType
=== 'touch';
1057 // Use ownerCmp as the upward link. Menus *must have no ownerCt* - they are global floaters.
1058 // Upward navigation is done using the up() method.
1059 menu
.activeHeader
= menu
.ownerCmp
= header
;
1060 header
.setMenuActive(menu
);
1062 // enable or disable asc & desc menu items based on header being sortable
1063 sortableMth
= header
.sortable
? 'enable' : 'disable';
1065 ascItem
[sortableMth
]();
1068 descItem
[sortableMth
]();
1071 // Pointer-invoked menus do not auto focus, key invoked ones do.
1072 menu
.autoFocus
= !clickEvent
|| clickEvent
.keyCode
;
1074 // For longpress t is the header, for click/hover t is the trigger
1075 menu
.showBy(t
, 'tl-bl?');
1077 // Menu show was vetoed by event handler - clear context
1078 if (!menu
.isVisible()) {
1079 this.onMenuHide(menu
);
1083 hideMenu: function() {
1089 // remove the trigger open class when the menu is hidden
1090 onMenuHide: function(menu
) {
1091 menu
.activeHeader
.setMenuActive(false);
1094 purgeHeaderCtCache: function (headerCt
) {
1096 headerCt
.purgeCache();
1097 if (headerCt
.isRootHeader
) {
1100 headerCt
= headerCt
.ownerCt
;
1104 purgeCache: function() {
1106 visibleColumnManager
= me
.visibleColumnManager
,
1107 columnManager
= me
.columnManager
;
1109 // Delete column cache - column order has changed.
1110 me
.gridVisibleColumns
= me
.gridDataColumns
= me
.hideableColumns
= null;
1112 // ColumnManager. Only the top
1113 if (visibleColumnManager
) {
1114 visibleColumnManager
.invalidate();
1115 columnManager
.invalidate();
1120 * Gets the menu (and will create it if it doesn't already exist)
1123 getMenu: function() {
1125 grid
= me
.view
&& me
.view
.ownerGrid
;
1128 me
.menu
= new Ext
.menu
.Menu({
1129 hideOnParentHide
: false, // Persists when owning ColumnHeader is hidden
1130 items
: me
.getMenuItems(),
1132 beforeshow
: me
.beforeMenuShow
,
1133 hide
: me
.onMenuHide
,
1137 me
.fireEvent('menucreate', me
, me
.menu
);
1139 grid
.fireEvent('headermenucreate', grid
, me
.menu
, me
);
1145 // Render our menus to the first enclosing scrolling element so that they scroll with the grid
1146 beforeMenuShow: function(menu
) {
1148 columnItem
= menu
.child('#columnItem'),
1152 // If a change of column structure caused destruction of the column menu item
1153 // or the main menu was created without the column menu item because it began with no hideable headers
1154 // Then create it and its menu now.
1156 hideableColumns
= me
.enableColumnHide
? me
.getColumnMenu(me
) : null;
1158 // Insert after the "Sort Ascending", "Sort Descending" menu items if they are present.
1159 insertPoint
= me
.sortable
? 2 : 0;
1161 if (hideableColumns
&& hideableColumns
.length
) {
1162 menu
.insert(insertPoint
, [{
1163 itemId
: 'columnItemSeparator',
1164 xtype
: 'menuseparator'
1166 itemId
: 'columnItem',
1167 text
: me
.columnsText
,
1168 iconCls
: me
.menuColsIcon
,
1170 items
: hideableColumns
1177 me
.updateMenuDisabledState(me
.menu
);
1178 // TODO: rendering the menu to the nearest overlfowing ancestor has been disabled
1179 // since DomQuery is no longer available by default in 5.0
1180 // if (!menu.rendered) {
1181 // menu.render(this.el.up('{overflow=auto}') || document.body);
1186 * Returns an array of menu items to be placed into the shared menu
1187 * across all headers in this header container.
1188 * @return {Array} menuItems
1190 getMenuItems: function() {
1193 hideableColumns
= me
.enableColumnHide
? me
.getColumnMenu(me
) : null;
1198 text
: me
.sortAscText
,
1199 iconCls
: me
.menuSortAscCls
,
1200 handler
: me
.onSortAscClick
,
1204 text
: me
.sortDescText
,
1205 iconCls
: me
.menuSortDescCls
,
1206 handler
: me
.onSortDescClick
,
1210 if (hideableColumns
&& hideableColumns
.length
) {
1213 itemId
: 'columnItemSeparator',
1214 xtype
: 'menuseparator'
1218 itemId
: 'columnItem',
1219 text
: me
.columnsText
,
1220 iconCls
: me
.menuColsIcon
,
1221 menu
: hideableColumns
,
1228 // sort asc when clicking on item in menu
1229 onSortAscClick: function() {
1230 var menu
= this.getMenu(),
1231 activeHeader
= menu
.activeHeader
;
1233 activeHeader
.sort('ASC');
1236 // sort desc when clicking on item in menu
1237 onSortDescClick: function() {
1238 var menu
= this.getMenu(),
1239 activeHeader
= menu
.activeHeader
;
1241 activeHeader
.sort('DESC');
1245 * Returns an array of menu CheckItems corresponding to all immediate children
1246 * of the passed Container which have been configured as hideable.
1248 getColumnMenu: function(headerContainer
) {
1252 items
= headerContainer
.query('>gridcolumn[hideable]'),
1253 itemsLn
= items
.length
,
1256 for (; i
< itemsLn
; i
++) {
1258 menuItem
= new Ext
.menu
.CheckItem({
1259 text
: item
.menuText
|| item
.text
,
1260 checked
: !item
.hidden
,
1263 menu
: item
.isGroupHeader
? this.getColumnMenu(item
) : undefined,
1264 checkHandler
: this.onColumnCheckChange
,
1267 menuItems
.push(menuItem
);
1269 // Prevent creating a submenu if we have no items
1270 return menuItems
.length
? menuItems
: null;
1273 onColumnCheckChange: function(checkItem
, checked
) {
1274 var header
= Ext
.getCmp(checkItem
.headerId
);
1276 if (header
.rendered
) {
1277 header
[checked
? 'show' : 'hide']();
1279 header
.hidden
= !checked
;
1284 * Returns the number of <b>grid columns</b> descended from this HeaderContainer.
1285 * Group Columns are HeaderContainers. All grid columns are returned, including hidden ones.
1287 getColumnCount: function() {
1288 return this.getGridColumns().length
;
1292 * Gets the full width of all columns that are visible for setting width of tables.
1294 getTableWidth: function() {
1296 headers
= this.getVisibleGridColumns(),
1297 headersLn
= headers
.length
,
1300 for (i
= 0; i
< headersLn
; i
++) {
1301 fullWidth
+= headers
[i
].getCellWidth() || 0;
1307 * Returns an array of the **visible** columns in the grid. This goes down to the
1308 * lowest column header level, and does not return **grouped** headers which contain
1311 * See also {@link Ext.grid.header.Container#getGridColumns}
1312 * @return {Ext.grid.column.Column[]} columns An array of visible columns. Returns
1313 * an empty array if no visible columns are found.
1315 getVisibleGridColumns: function() {
1317 allColumns
, rootHeader
,
1318 result
, len
, i
, column
;
1320 if (me
.gridVisibleColumns
) {
1321 return me
.gridVisibleColumns
;
1324 allColumns
= me
.getGridColumns();
1325 rootHeader
= me
.getRootHeaderCt();
1327 len
= allColumns
.length
;
1329 // Use an inline check instead of ComponentQuery filtering for better performance for
1330 // repeated grid row rendering - as in buffered rendering.
1331 for (i
= 0; i
< len
; i
++) {
1332 column
= allColumns
[i
];
1334 if (!column
.hidden
&& !column
.isColumnHidden(rootHeader
)) {
1335 result
[result
.length
] = column
;
1339 me
.gridVisibleColumns
= result
;
1344 isColumnHidden: function(rootHeader
) {
1345 var owner
= this.getRefOwner();
1346 while (owner
&& owner
!== rootHeader
) {
1350 owner
= owner
.getRefOwner();
1356 * @method getGridColumns
1357 * Returns an array of all columns which exist in the grid's View, visible or not.
1358 * This goes down to the leaf column header level, and does not return **grouped**
1359 * headers which contain sub headers.
1361 * It includes hidden headers even though they are not rendered. This is for
1362 * collection of menu items for the column hide/show menu.
1364 * Headers which have a hidden ancestor have a `hiddenAncestor: true` property
1365 * injected so that descendants are known to be hidden without interrogating that
1366 * header's ownerCt axis for a hidden ancestor.
1368 * See also {@link Ext.grid.header.Container#getVisibleGridColumns}
1369 * @return {Ext.grid.column.Column[]} columns An array of columns. Returns an
1370 * empty array if no columns are found.
1373 getGridColumns: function(/* private - used in recursion*/inResult
, hiddenAncestor
) {
1374 if (!inResult
&& this.gridDataColumns
) {
1375 return this.gridDataColumns
;
1379 result
= inResult
|| [],
1380 items
, i
, len
, item
,
1383 hiddenAncestor
= hiddenAncestor
|| me
.hidden
;
1385 items
= me
.items
.items
;
1387 // An ActionColumn (Columns extend HeaderContainer) may have an items *array* being the action items that it renders.
1389 for (i
= 0, len
= items
.length
; i
< len
; i
++) {
1391 if (item
.isGroupHeader
) {
1392 // Group headers will need a visibleIndex for if/when they're removed from their owner.
1393 // See Ext.layout.container.Container#moveItemBefore.
1394 item
.visibleIndex
= result
.length
;
1395 item
.getGridColumns(result
, hiddenAncestor
);
1397 item
.hiddenAncestor
= hiddenAncestor
;
1404 me
.gridDataColumns
= result
;
1407 // If top level, correct first and last visible column flags
1408 if (!inResult
&& len
) {
1409 // Set firstVisible and lastVisible flags
1410 for (i
= 0, len
= result
.length
; i
< len
; i
++) {
1413 // The column index within all (visible AND hidden) leaf level columns.
1414 // Used as the cellIndex in TableView's cell renderer call
1415 item
.fullColumnIndex
= i
;
1416 item
.isFirstVisible
= item
.isLastVisible
= false;
1417 if (!(item
.hidden
|| item
.hiddenAncestor
)) {
1418 if (!lastVisibleColumn
) {
1419 item
.isFirstVisible
= true;
1421 lastVisibleColumn
= item
;
1424 // If we haven't hidden all columns, tag the last visible one encountered
1425 if (lastVisibleColumn
) {
1426 lastVisibleColumn
.isLastVisible
= true;
1435 * For use by column headers in determining whether there are any hideable columns when deciding whether or not
1436 * the header menu should be disabled.
1438 getHideableColumns: function() {
1440 result
= me
.hideableColumns
;
1443 result
= me
.hideableColumns
= me
.query('[hideable]');
1449 * Returns the index of a leaf level header regardless of what the nesting
1452 * If a group header is passed, the index of the first leaf level header within it is returned.
1454 * @param {Ext.grid.column.Column} header The header to find the index of
1455 * @return {Number} The index of the specified column header
1457 getHeaderIndex: function (header
) {
1458 // Binding the columnManager to a column makes it backwards-compatible with versions
1459 // that only bind the columnManager to a root header.
1460 if (!this.columnManager
) {
1461 this.columnManager
= this.getRootHeaderCt().columnManager
;
1464 return this.columnManager
.getHeaderIndex(header
);
1468 * Get a leaf level header by index regardless of what the nesting
1471 * @param {Number} index The column index for which to retrieve the column.
1473 getHeaderAtIndex: function (index
) {
1474 // Binding the columnManager to a column makes it backwards-compatible with versions
1475 // that only bind the columnManager to a root header.
1476 if (!this.columnManager
) {
1477 this.columnManager
= this.getRootHeaderCt().columnManager
;
1480 return this.columnManager
.getHeaderAtIndex(index
);
1484 * When passed a column index, returns the closet *visible* column to that. If the column at the passed index is visible,
1485 * that is returned. If it is hidden, either the next visible, or the previous visible column is returned.
1487 * @param {Number} index Position at which to find the closest visible column.
1489 getVisibleHeaderClosestToIndex: function (index
) {
1490 // Binding the columnManager to a column makes it backwards-compatible with versions
1491 // that only bind the columnManager to a root header.
1492 if (!this.visibleColumnManager
) {
1493 this.visibleColumnManager
= this.getRootHeaderCt().visibleColumnManager
;
1496 return this.visibleColumnManager
.getVisibleHeaderClosestToIndex(index
);
1499 applyForceFit: function (header
) {
1502 minWidth
= Ext
.grid
.plugin
.HeaderResizer
.prototype.minColWidth
,
1503 // Used when a column's max contents are larger than the available view width.
1504 useMinWidthForFlex
= false,
1505 defaultWidth
= Ext
.grid
.header
.Container
.prototype.defaultWidth
,
1506 availFlex
= me
.el
.dom
.clientWidth
- (view
.el
.dom
.scrollHeight
> view
.el
.dom
.clientHeight
? Ext
.getScrollbarSize().width
: 0),
1508 items
= me
.getVisibleGridColumns(),
1509 hidden
= header
.hidden
,
1512 maxAvailFlexOneColumn
,
1515 function getTotalFlex() {
1516 for (i
= 0, len
= items
.length
; i
< len
; i
++) {
1519 // Skip the current header.
1520 if (item
=== header
) {
1524 item
.flex
= item
.flex
|| item
.width
|| item
.getWidth();
1525 totalFlex
+= item
.flex
;
1530 function applyWidth() {
1531 // The currently-sized column (whether resized or reshown) will already
1532 // have a width, so all other columns will need to be flexed.
1533 var isCurrentHeader
;
1535 for (i
= 0, len
= items
.length
; i
< len
; i
++) {
1537 isCurrentHeader
= (item
=== header
);
1539 if (useMinWidthForFlex
&& !isCurrentHeader
) {
1540 // The selected column is extremely large so set all the others as flex minWidth.
1541 item
.flex
= minWidth
;
1543 } else if (!isCurrentHeader
) {
1544 // Note that any widths MUST be converted to flex. Imagine that all but one columns
1545 // are hidden. The widths of each column very easily could be greater than the total
1546 // available width (think about the how visible header widths increase as sibling
1547 // columns are hidden), so they cannot be reliably used to size the header, and the only
1548 // safe approach is to convert any all widths to flex (except for the current header).
1549 myWidth
= item
.flex
|| defaultWidth
;
1550 item
.flex
= Math
.max(Math
.ceil((myWidth
/ totalFlex
) * availFlex
), minWidth
);
1554 item
.setWidth(item
.width
|| item
.flex
);
1558 Ext
.suspendLayouts();
1560 // Determine the max amount of flex that a single column can have.
1561 maxAvailFlexOneColumn
= (availFlex
- ((items
.length
+ 1) * minWidth
));
1563 // First, remove the header's flex as it should always receive a set width
1564 // since it is the header being operated on.
1568 myWidth
= header
.width
|| header
.savedWidth
;
1569 header
.savedWidth
= null;
1571 myWidth
= view
.getMaxContentWidth(header
);
1574 // We need to know if the max content width of the selected column would blow out the
1575 // grid. If so, all the other visible columns will be flexed to minWidth.
1576 if (myWidth
> maxAvailFlexOneColumn
) {
1577 header
.width
= maxAvailFlexOneColumn
;
1578 useMinWidthForFlex
= true;
1580 header
.width
= myWidth
;
1582 // Substract the current header's width from the available flex + some padding
1583 // to ensure that the last column doesn't get nudged out of the view.
1584 availFlex
-= myWidth
+ defaultWidth
;
1590 Ext
.resumeLayouts(true);
1593 autoSizeColumn: function (header
) {
1594 var view
= this.view
;
1597 view
.autoSizeColumn(header
);
1598 if (this.forceFit
) {
1599 this.applyForceFit(header
);
1604 getRefItems: function(deep
) {
1605 // Override to include the header menu in the component tree
1606 var result
= this.callParent([deep
]);
1609 result
.push(this.menu
);
1615 beginChildHide: function() {
1616 ++this.childHideCount
;
1619 endChildHide: function() {
1620 --this.childHideCount
;
1623 getFocusables: function() {
1624 return this.isRootHeader
?
1625 this.getVisibleGridColumns() :
1629 createFocusableContainerKeyNav: function(el
) {
1632 return new Ext
.util
.KeyNav(el
, {
1635 down
: me
.showHeaderMenu
,
1636 left
: me
.onFocusableContainerLeftKey
,
1637 right
: me
.onFocusableContainerRightKey
,
1641 space
: me
.onHeaderActivate
,
1642 enter
: me
.onHeaderActivate
1646 onHomeKey: function(e
) {
1647 return this.focusChild(null, true, e
);
1650 onEndKey: function(e
) {
1651 return this.focusChild(null, false, e
);
1654 showHeaderMenu: function(e
) {
1655 var column
= this.getFocusableFromEvent(e
);
1657 // DownArrow event must be from a column, not a Component within the column (eg filter fields)
1658 if (column
&& column
.isColumn
&& column
.triggerEl
) {
1659 this.onHeaderTriggerClick(column
, e
, column
.triggerEl
);
1663 onHeaderActivate: function(e
) {
1664 var column
= this.getFocusableFromEvent(e
),
1668 // Remember that not every descendant of a headerCt is a column! It could be a child component of a column.
1669 if (column
&& column
.isColumn
) {
1670 view
= column
.getView();
1672 // Sort the column is configured that way.
1673 // sortOnClick may be set to false by SpreadsheelSelectionModel to allow click to select a column.
1674 if (column
.sortable
&& this.sortOnClick
) {
1675 lastFocused
= view
.getNavigationModel().getLastFocused();
1676 column
.toggleSortState();
1678 // After keyboard sort, bring last focused record into view
1680 view
.ownerCt
.ensureVisible(lastFocused
.record
);
1683 // onHeaderClick is a necessary part of accessibility processing, sortable or not.
1684 this.onHeaderClick(column
, e
, column
.el
);
1688 onFocusableContainerMousedown: function(e
, target
) {
1689 var targetCmp
= Ext
.Component
.fromElement(target
);
1691 if (targetCmp
=== this) {
1694 // The DDManager (Header Containers are draggable) prevents mousedown default
1695 // So we must explicitly focus the header