3 This file is part of Ext JS 4
5 Copyright (c) 2011 Sencha Inc
7 Contact: http://www.sencha.com/contact
9 GNU General Public License Usage
10 This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file. Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
12 If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
16 * @class Ext.view.Table
17 * @extends Ext.view.View
19 This class encapsulates the user interface for a tabular data set.
20 It acts as a centralized manager for controlling the various interface
21 elements of the view. This includes handling events, such as row and cell
22 level based DOM events. It also reacts to events from the underlying {@link Ext.selection.Model}
23 to provide visual feedback to the user.
25 This class does not provide ways to manipulate the underlying data of the configured
26 {@link Ext.data.Store}.
28 This is the base class for both {@link Ext.grid.View} and {@link Ext.tree.View} and is not
33 * @author Nicolas Ferrero
35 Ext.define('Ext.view.Table', {
36 extend: 'Ext.view.View',
37 alias: 'widget.tableview',
39 'Ext.view.TableChunker',
40 'Ext.util.DelayedTask',
41 'Ext.util.MixedCollection'
44 cls: Ext.baseCSSPrefix + 'grid-view',
47 itemSelector: '.' + Ext.baseCSSPrefix + 'grid-row',
49 cellSelector: '.' + Ext.baseCSSPrefix + 'grid-cell',
51 selectedItemCls: Ext.baseCSSPrefix + 'grid-row-selected',
52 selectedCellCls: Ext.baseCSSPrefix + 'grid-cell-selected',
53 focusedItemCls: Ext.baseCSSPrefix + 'grid-row-focused',
54 overItemCls: Ext.baseCSSPrefix + 'grid-row-over',
55 altRowCls: Ext.baseCSSPrefix + 'grid-row-alt',
56 rowClsRe: /(?:^|\s*)grid-row-(first|last|alt)(?:\s+|$)/g,
57 cellRe: new RegExp('x-grid-cell-([^\\s]+) ', ''),
63 * Override this function to apply custom CSS classes to rows during rendering. You can also supply custom
64 * parameters to the row template for the current row to customize how it is rendered using the <b>rowParams</b>
65 * parameter. This function should return the CSS class name (or empty string '' for none) that will be added
66 * to the row's wrapping div. To apply multiple class names, simply return them space-delimited within the string
67 * (e.g., 'my-class another-class'). Example usage:
71 showPreview: true, // custom property
72 enableRowBody: true, // required to create a second, full-width row to show expanded Record data
73 getRowClass: function(record, rowIndex, rp, ds){ // rp = rowParams
75 rp.body = '<p>'+record.data.excerpt+'</p>';
76 return 'x-grid3-row-expanded';
78 return 'x-grid3-row-collapsed';
82 * @param {Model} model The {@link Ext.data.Model} corresponding to the current row.
83 * @param {Number} index The row index.
84 * @param {Object} rowParams (DEPRECATED) A config object that is passed to the row template during rendering that allows
85 * customization of various aspects of a grid row.
86 * <p>If {@link #enableRowBody} is configured <b><tt></tt>true</b>, then the following properties may be set
87 * by this function, and will be used to render a full-width expansion row below each grid row:</p>
89 * <li><code>body</code> : String <div class="sub-desc">An HTML fragment to be used as the expansion row's body content (defaults to '').</div></li>
90 * <li><code>bodyStyle</code> : String <div class="sub-desc">A CSS style specification that will be applied to the expansion row's <tr> element. (defaults to '').</div></li>
92 * The following property will be passed in, and may be appended to:
94 * <li><code>tstyle</code> : String <div class="sub-desc">A CSS style specification that willl be applied to the <table> element which encapsulates
95 * both the standard grid row, and any expansion row.</div></li>
97 * @param {Store} store The {@link Ext.data.Store} this grid is bound to
99 * @return {String} a CSS class name to add to the row.
103 initComponent: function() {
106 if (me.deferRowRender !== false) {
107 me.refresh = function() {
109 setTimeout(function() {
116 me.selModel.view = me;
117 me.headerCt.view = me;
119 me.tpl = '<div></div>';
122 load: me.onStoreLoad,
129 // * @param {Ext.data.Record} record
130 // * @param {HTMLElement} row
131 // * @param {Number} rowIdx
137 // scroll to top of the grid when store loads
138 onStoreLoad: function(){
141 if (me.invalidateScrollerOnRefresh) {
143 if (!me.scrollToTopTask) {
144 me.scrollToTopTask = Ext.create('Ext.util.DelayedTask', me.scrollToTop, me);
146 me.scrollToTopTask.delay(1);
153 // scroll the view to the top
154 scrollToTop: Ext.emptyFn,
157 * Add a listener to the main view element. It will be destroyed with the view.
160 addElListener: function(eventName, fn, scope){
161 this.mon(this, eventName, fn, scope, {
167 * Get the columns used for generating a template via TableChunker.
168 * See {@link Ext.grid.header.Container#getGridColumns}.
171 getGridColumns: function() {
172 return this.headerCt.getGridColumns();
176 * Get a leaf level header by index regardless of what the nesting
179 * @param {Number} index The index
181 getHeaderAtIndex: function(index) {
182 return this.headerCt.getHeaderAtIndex(index);
186 * Get the cell (td) for a particular record and column.
187 * @param {Ext.data.Model} record
188 * @param {Ext.grid.column.Colunm} column
191 getCell: function(record, column) {
192 var row = this.getNode(record);
193 return Ext.fly(row).down(column.getCellSelector());
197 * Get a reference to a feature
198 * @param {String} id The id of the feature
199 * @return {Ext.grid.feature.Feature} The feature. Undefined if not found
201 getFeature: function(id) {
202 var features = this.featuresMC;
204 return features.get(id);
209 * Initializes each feature and bind it to this view.
212 initFeatures: function() {
218 me.features = me.features || [];
219 features = me.features;
220 len = features.length;
222 me.featuresMC = Ext.create('Ext.util.MixedCollection');
223 for (; i < len; i++) {
224 // ensure feature hasnt already been instantiated
225 if (!features[i].isFeature) {
226 features[i] = Ext.create('feature.' + features[i].ftype, features[i]);
228 // inject a reference to view
229 features[i].view = me;
230 me.featuresMC.add(features[i]);
235 * Gives features an injection point to attach events to the markup that
236 * has been created for this view.
239 attachEventsForFeatures: function() {
240 var features = this.features,
241 ln = features.length,
244 for (; i < ln; i++) {
245 if (features[i].isFeature) {
246 features[i].attachEvents();
251 afterRender: function() {
256 scroll: me.fireBodyScroll,
259 me.el.unselectable();
260 me.attachEventsForFeatures();
263 fireBodyScroll: function(e, t) {
264 this.fireEvent('bodyscroll', e, t);
267 // TODO: Refactor headerCt dependency here to colModel
269 * Uses the headerCt to transform data from dataIndex keys in a record to
270 * headerId keys in each header and then run them through each feature to
271 * get additional data for variables they have injected into the view template.
274 prepareData: function(data, idx, record) {
276 orig = me.headerCt.prepareData(data, idx, record, me, me.ownerCt),
277 features = me.features,
278 ln = features.length,
282 for (; i < ln; i++) {
283 feature = features[i];
284 if (feature.isFeature) {
285 Ext.apply(orig, feature.getAdditionalData(data, idx, record, orig, me));
292 // TODO: Refactor headerCt dependency here to colModel
293 collectData: function(records, startIndex) {
294 var preppedRecords = this.callParent(arguments),
295 headerCt = this.headerCt,
296 fullWidth = headerCt.getFullWidth(),
297 features = this.features,
298 ln = features.length,
300 rows: preppedRecords,
309 jln = preppedRecords.length;
310 // process row classes, rowParams has been deprecated and has been moved
311 // to the individual features that implement the behavior.
312 if (this.getRowClass) {
313 for (; j < jln; j++) {
315 preppedRecords[j]['rowCls'] = this.getRowClass(records[j], j, rowParams, this.store);
318 Ext.Error.raise("The getRowClass alt property is no longer supported.");
320 if (rowParams.tstyle) {
321 Ext.Error.raise("The getRowClass tstyle property is no longer supported.");
323 if (rowParams.cells) {
324 Ext.Error.raise("The getRowClass cells property is no longer supported.");
326 if (rowParams.body) {
327 Ext.Error.raise("The getRowClass body property is no longer supported. Use the getAdditionalData method of the rowbody feature.");
329 if (rowParams.bodyStyle) {
330 Ext.Error.raise("The getRowClass bodyStyle property is no longer supported.");
332 if (rowParams.cols) {
333 Ext.Error.raise("The getRowClass cols property is no longer supported.");
338 // currently only one feature may implement collectData. This is to modify
339 // what's returned to the view before its rendered
340 for (; i < ln; i++) {
341 feature = features[i];
342 if (feature.isFeature && feature.collectData && !feature.disabled) {
343 o = feature.collectData(records, preppedRecords, startIndex, fullWidth, o);
350 // TODO: Refactor header resizing to column resizing
352 * When a header is resized, setWidth on the individual columns resizer class,
353 * the top level table, save/restore scroll state, generate a new template and
354 * restore focus to the grid view's element so that keyboard navigation
358 onHeaderResize: function(header, w, suppressFocus) {
363 me.saveScrollState();
364 // Grab the col and set the width, css
365 // class is generated in TableChunker.
366 // Select composites because there may be several chunks.
369 // Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy.
370 // We need to increment the passed with in this case.
371 if (Ext.isIE6 || Ext.isIE7) {
372 if (header.el.hasCls(Ext.baseCSSPrefix + 'column-header-first')) {
376 el.select('.' + Ext.baseCSSPrefix + 'grid-col-resizer-'+header.id).setWidth(w);
377 el.select('.' + Ext.baseCSSPrefix + 'grid-table-resizer').setWidth(me.headerCt.getFullWidth());
378 me.restoreScrollState();
380 if (!suppressFocus) {
387 * When a header is shown restore its oldWidth if it was previously hidden.
390 onHeaderShow: function(headerCt, header, suppressFocus) {
391 // restore headers that were dynamically hidden
392 if (header.oldWidth) {
393 this.onHeaderResize(header, header.oldWidth, suppressFocus);
394 delete header.oldWidth;
395 // flexed headers will have a calculated size set
396 // this additional check has to do with the fact that
397 // defaults: {width: 100} will fight with a flex value
398 } else if (header.width && !header.flex) {
399 this.onHeaderResize(header, header.width, suppressFocus);
401 this.setNewTemplate();
405 * When the header hides treat it as a resize to 0.
408 onHeaderHide: function(headerCt, header, suppressFocus) {
409 this.onHeaderResize(header, 0, suppressFocus);
413 * Set a new template based on the current columns displayed in the
417 setNewTemplate: function() {
419 columns = me.headerCt.getColumnsForTpl(true);
421 me.tpl = me.getTableChunker().getTableTpl({
423 features: me.features
428 * Get the configured chunker or default of Ext.view.TableChunker
430 getTableChunker: function() {
431 return this.chunker || Ext.view.TableChunker;
435 * Add a CSS Class to a specific row.
436 * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model representing this row
437 * @param {String} cls
439 addRowCls: function(rowInfo, cls) {
440 var row = this.getNode(rowInfo);
442 Ext.fly(row).addCls(cls);
447 * Remove a CSS Class from a specific row.
448 * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model representing this row
449 * @param {String} cls
451 removeRowCls: function(rowInfo, cls) {
452 var row = this.getNode(rowInfo);
454 Ext.fly(row).removeCls(cls);
458 // GridSelectionModel invokes onRowSelect as selection changes
459 onRowSelect : function(rowIdx) {
460 this.addRowCls(rowIdx, this.selectedItemCls);
463 // GridSelectionModel invokes onRowDeselect as selection changes
464 onRowDeselect : function(rowIdx) {
467 me.removeRowCls(rowIdx, me.selectedItemCls);
468 me.removeRowCls(rowIdx, me.focusedItemCls);
471 onCellSelect: function(position) {
472 var cell = this.getCellByPosition(position);
474 cell.addCls(this.selectedCellCls);
478 onCellDeselect: function(position) {
479 var cell = this.getCellByPosition(position);
481 cell.removeCls(this.selectedCellCls);
486 onCellFocus: function(position) {
487 //var cell = this.getCellByPosition(position);
488 this.focusCell(position);
491 getCellByPosition: function(position) {
492 var row = position.row,
493 column = position.column,
495 node = this.getNode(row),
496 header = this.headerCt.getHeaderAtIndex(column),
500 if (header && node) {
501 cellSelector = header.getCellSelector();
502 cell = Ext.fly(node).down(cellSelector);
507 // GridSelectionModel invokes onRowFocus to 'highlight'
508 // the last row focused
509 onRowFocus: function(rowIdx, highlight, supressFocus) {
511 row = me.getNode(rowIdx);
514 me.addRowCls(rowIdx, me.focusedItemCls);
518 //this.el.dom.setAttribute('aria-activedescendant', row.id);
520 me.removeRowCls(rowIdx, me.focusedItemCls);
525 * Focus a particular row and bring it into view. Will fire the rowfocus event.
526 * @param {Mixed} rowIdx An HTMLElement template node, index of a template node, the
527 * id of a template node or the record associated with the node.
529 focusRow: function(rowIdx) {
531 row = me.getNode(rowIdx),
540 elRegion = el.getRegion();
541 rowRegion = Ext.fly(row).getRegion();
543 if (rowRegion.top < elRegion.top) {
544 adjustment = rowRegion.top - elRegion.top;
546 } else if (rowRegion.bottom > elRegion.bottom) {
547 adjustment = rowRegion.bottom - elRegion.bottom;
549 record = me.getRecord(row);
550 rowIdx = me.store.indexOf(record);
553 // scroll the grid itself, so that all gridview's update.
554 panel.scrollByDeltaY(adjustment);
556 me.fireEvent('rowfocus', record, row, rowIdx);
560 focusCell: function(position) {
562 cell = me.getCellByPosition(position),
566 elRegion = el.getRegion(),
572 cellRegion = cell.getRegion();
574 if (cellRegion.top < elRegion.top) {
575 adjustmentY = cellRegion.top - elRegion.top;
577 } else if (cellRegion.bottom > elRegion.bottom) {
578 adjustmentY = cellRegion.bottom - elRegion.bottom;
582 if (cellRegion.left < elRegion.left) {
583 adjustmentX = cellRegion.left - elRegion.left;
585 } else if (cellRegion.right > elRegion.right) {
586 adjustmentX = cellRegion.right - elRegion.right;
590 // scroll the grid itself, so that all gridview's update.
591 panel.scrollByDeltaY(adjustmentY);
594 panel.scrollByDeltaX(adjustmentX);
597 me.fireEvent('cellfocus', record, cell, position);
602 * Scroll by delta. This affects this individual view ONLY and does not
603 * synchronize across views or scrollers.
604 * @param {Number} delta
605 * @param {String} dir (optional) Valid values are scrollTop and scrollLeft. Defaults to scrollTop.
608 scrollByDelta: function(delta, dir) {
609 dir = dir || 'scrollTop';
610 var elDom = this.el.dom;
611 elDom[dir] = (elDom[dir] += delta);
614 onUpdate: function(ds, index) {
615 this.callParent(arguments);
619 * Save the scrollState in a private variable.
620 * Must be used in conjunction with restoreScrollState
622 saveScrollState: function() {
623 var dom = this.el.dom,
624 state = this.scrollState;
626 state.left = dom.scrollLeft;
627 state.top = dom.scrollTop;
631 * Restore the scrollState.
632 * Must be used in conjunction with saveScrollState
635 restoreScrollState: function() {
636 var dom = this.el.dom,
637 state = this.scrollState,
638 headerEl = this.headerCt.el.dom;
640 headerEl.scrollLeft = dom.scrollLeft = state.left;
641 dom.scrollTop = state.top;
645 * Refresh the grid view.
646 * Saves and restores the scroll state, generates a new template, stripes rows
647 * and invalidates the scrollers.
649 refresh: function() {
650 this.setNewTemplate();
651 this.callParent(arguments);
654 processItemEvent: function(record, row, rowIndex, e) {
656 cell = e.getTarget(me.cellSelector, row),
657 cellIndex = cell ? cell.cellIndex : -1,
658 map = me.statics().EventMap,
659 selModel = me.getSelectionModel(),
663 if (type == 'keydown' && !cell && selModel.getCurrentPosition) {
664 // CellModel, otherwise we can't tell which cell to invoke
665 cell = me.getCellByPosition(selModel.getCurrentPosition());
668 cellIndex = cell.cellIndex;
672 result = me.fireEvent('uievent', type, me, cell, rowIndex, cellIndex, e);
674 if (result === false || me.callParent(arguments) === false) {
678 // Don't handle cellmouseenter and cellmouseleave events for now
679 if (type == 'mouseover' || type == 'mouseout') {
684 // We are adding cell and feature events
685 (me['onBeforeCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
686 (me.fireEvent('beforecell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false) ||
687 (me['onCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
688 (me.fireEvent('cell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false)
692 processSpecialEvent: function(e) {
694 map = me.statics().EventMap,
695 features = me.features,
696 ln = features.length,
698 i, feature, prefix, featureTarget,
702 me.callParent(arguments);
704 if (type == 'mouseover' || type == 'mouseout') {
708 for (i = 0; i < ln; i++) {
709 feature = features[i];
710 if (feature.hasFeatureEvent) {
711 featureTarget = e.getTarget(feature.eventSelector, me.getTargetEl());
713 prefix = feature.eventPrefix;
714 // allows features to implement getFireEventArgs to change the
715 // fireEvent signature
716 beforeArgs = feature.getFireEventArgs('before' + prefix + type, me, featureTarget, e);
717 args = feature.getFireEventArgs(prefix + type, me, featureTarget, e);
721 (me.fireEvent.apply(me, beforeArgs) === false) ||
723 (panel.fireEvent.apply(panel, beforeArgs) === false) ||
725 (me.fireEvent.apply(me, args) === false) ||
727 (panel.fireEvent.apply(panel, args) === false)
737 onCellMouseDown: Ext.emptyFn,
738 onCellMouseUp: Ext.emptyFn,
739 onCellClick: Ext.emptyFn,
740 onCellDblClick: Ext.emptyFn,
741 onCellContextMenu: Ext.emptyFn,
742 onCellKeyDown: Ext.emptyFn,
743 onBeforeCellMouseDown: Ext.emptyFn,
744 onBeforeCellMouseUp: Ext.emptyFn,
745 onBeforeCellClick: Ext.emptyFn,
746 onBeforeCellDblClick: Ext.emptyFn,
747 onBeforeCellContextMenu: Ext.emptyFn,
748 onBeforeCellKeyDown: Ext.emptyFn,
751 * Expand a particular header to fit the max content width.
752 * This will ONLY expand, not contract.
755 expandToFit: function(header) {
757 var maxWidth = this.getMaxContentWidth(header);
759 header.setWidth(maxWidth);
764 * Get the max contentWidth of the header's text and all cells
765 * in the grid under this header.
768 getMaxContentWidth: function(header) {
769 var cellSelector = header.getCellInnerSelector(),
770 cells = this.el.query(cellSelector),
773 maxWidth = header.el.dom.scrollWidth,
776 for (; i < ln; i++) {
777 scrollWidth = cells[i].scrollWidth;
778 if (scrollWidth > maxWidth) {
779 maxWidth = scrollWidth;
785 getPositionByEvent: function(e) {
787 cellNode = e.getTarget(me.cellSelector),
788 rowNode = e.getTarget(me.itemSelector),
789 record = me.getRecord(rowNode),
790 header = me.getHeaderByCell(cellNode);
792 return me.getPosition(record, header);
795 getHeaderByCell: function(cell) {
797 var m = cell.className.match(this.cellRe);
799 return Ext.getCmp(m[1]);
806 * @param {Object} position The current row and column: an object containing the following properties:<ul>
807 * <li>row<div class="sub-desc"> The row <b>index</b></div></li>
808 * <li>column<div class="sub-desc">The column <b>index</b></div></li>
810 * @param {String} direction 'up', 'down', 'right' and 'left'
811 * @param {Ext.EventObject} e event
812 * @param {Boolean} preventWrap Set to true to prevent wrap around to the next or previous row.
813 * @param {Function} verifierFn A function to verify the validity of the calculated position. When using this function, you must return true to allow the newPosition to be returned.
814 * @param {Scope} scope Scope to run the verifierFn in
815 * @returns {Object} newPosition An object containing the following properties:<ul>
816 * <li>row<div class="sub-desc"> The row <b>index</b></div></li>
817 * <li>column<div class="sub-desc">The column <b>index</b></div></li>
821 walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {
825 rowCount = me.store.getCount(),
826 firstCol = me.getFirstVisibleColumnIndex(),
827 lastCol = me.getLastVisibleColumnIndex(),
828 newPos = {row: row, column: column},
829 activeHeader = me.headerCt.getHeaderAtIndex(column);
831 // no active header or its currently hidden
832 if (!activeHeader || activeHeader.hidden) {
837 direction = direction.toLowerCase();
840 // has the potential to wrap if its last
841 if (column === lastCol) {
842 // if bottom row and last column, deny right
843 if (preventWrap || row === rowCount - 1) {
847 // otherwise wrap to nextRow and firstCol
848 newPos.row = row + 1;
849 newPos.column = firstCol;
854 newPos.column = column + me.getRightGap(activeHeader);
856 newPos.column = lastCol;
862 // has the potential to wrap
863 if (column === firstCol) {
864 // if top row and first column, deny left
865 if (preventWrap || row === 0) {
869 // otherwise wrap to prevRow and lastCol
870 newPos.row = row - 1;
871 newPos.column = lastCol;
876 newPos.column = column + me.getLeftGap(activeHeader);
878 newPos.column = firstCol;
884 // if top row, deny up
890 newPos.row = row - 1;
898 // if bottom row, deny down
899 if (row === rowCount - 1) {
904 newPos.row = row + 1;
906 newPos.row = rowCount - 1;
912 if (verifierFn && verifierFn.call(scope || window, newPos) !== true) {
918 getFirstVisibleColumnIndex: function() {
919 var headerCt = this.getHeaderCt(),
920 allColumns = headerCt.getGridColumns(),
921 visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
922 firstHeader = visHeaders[0];
924 return headerCt.getHeaderIndex(firstHeader);
927 getLastVisibleColumnIndex: function() {
928 var headerCt = this.getHeaderCt(),
929 allColumns = headerCt.getGridColumns(),
930 visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
931 lastHeader = visHeaders[visHeaders.length - 1];
933 return headerCt.getHeaderIndex(lastHeader);
936 getHeaderCt: function() {
937 return this.headerCt;
940 getPosition: function(record, header) {
943 gridCols = me.headerCt.getGridColumns();
946 row: store.indexOf(record),
947 column: Ext.Array.indexOf(gridCols, header)
952 * Determines the 'gap' between the closest adjacent header to the right
953 * that is not hidden.
956 getRightGap: function(activeHeader) {
957 var headerCt = this.getHeaderCt(),
958 headers = headerCt.getGridColumns(),
959 activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
960 i = activeHeaderIdx + 1,
963 for (; i <= headers.length; i++) {
964 if (!headers[i].hidden) {
970 return nextIdx - activeHeaderIdx;
973 beforeDestroy: function() {
975 this.el.removeAllListeners();
977 this.callParent(arguments);
981 * Determines the 'gap' between the closest adjacent header to the left
982 * that is not hidden.
985 getLeftGap: function(activeHeader) {
986 var headerCt = this.getHeaderCt(),
987 headers = headerCt.getGridColumns(),
988 activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
989 i = activeHeaderIdx - 1,
992 for (; i >= 0; i--) {
993 if (!headers[i].hidden) {
999 return prevIdx - activeHeaderIdx;