2 * @class Ext.view.Table
3 * @extends Ext.view.View
5 This class encapsulates the user interface for a tabular data set.
6 It acts as a centralized manager for controlling the various interface
7 elements of the view. This includes handling events, such as row and cell
8 level based DOM events. It also reacts to events from the underlying {@link Ext.selection.Model}
9 to provide visual feedback to the user.
11 This class does not provide ways to manipulate the underlying data of the configured
12 {@link Ext.data.Store}.
14 This is the base class for both {@link Ext.grid.View} and {@link Ext.tree.View} and is not
20 * @author Nicolas Ferrero
22 Ext.define('Ext.view.Table', {
23 extend: 'Ext.view.View',
24 alias: 'widget.tableview',
26 'Ext.view.TableChunker',
27 'Ext.util.DelayedTask',
28 'Ext.util.MixedCollection'
31 cls: Ext.baseCSSPrefix + 'grid-view',
34 itemSelector: '.' + Ext.baseCSSPrefix + 'grid-row',
36 cellSelector: '.' + Ext.baseCSSPrefix + 'grid-cell',
38 selectedItemCls: Ext.baseCSSPrefix + 'grid-row-selected',
39 selectedCellCls: Ext.baseCSSPrefix + 'grid-cell-selected',
40 focusedItemCls: Ext.baseCSSPrefix + 'grid-row-focused',
41 overItemCls: Ext.baseCSSPrefix + 'grid-row-over',
42 altRowCls: Ext.baseCSSPrefix + 'grid-row-alt',
43 rowClsRe: /(?:^|\s*)grid-row-(first|last|alt)(?:\s+|$)/g,
44 cellRe: new RegExp('x-grid-cell-([^\\s]+) ', ''),
50 * Override this function to apply custom CSS classes to rows during rendering. You can also supply custom
51 * parameters to the row template for the current row to customize how it is rendered using the <b>rowParams</b>
52 * parameter. This function should return the CSS class name (or empty string '' for none) that will be added
53 * to the row's wrapping div. To apply multiple class names, simply return them space-delimited within the string
54 * (e.g., 'my-class another-class'). Example usage:
58 showPreview: true, // custom property
59 enableRowBody: true, // required to create a second, full-width row to show expanded Record data
60 getRowClass: function(record, rowIndex, rp, ds){ // rp = rowParams
62 rp.body = '<p>'+record.data.excerpt+'</p>';
63 return 'x-grid3-row-expanded';
65 return 'x-grid3-row-collapsed';
69 * @param {Model} model The {@link Ext.data.Model} corresponding to the current row.
70 * @param {Number} index The row index.
71 * @param {Object} rowParams (DEPRECATED) A config object that is passed to the row template during rendering that allows
72 * customization of various aspects of a grid row.
73 * <p>If {@link #enableRowBody} is configured <b><tt></tt>true</b>, then the following properties may be set
74 * by this function, and will be used to render a full-width expansion row below each grid row:</p>
76 * <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>
77 * <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>
79 * The following property will be passed in, and may be appended to:
81 * <li><code>tstyle</code> : String <div class="sub-desc">A CSS style specification that willl be applied to the <table> element which encapsulates
82 * both the standard grid row, and any expansion row.</div></li>
84 * @param {Store} store The {@link Ext.data.Store} this grid is bound to
86 * @return {String} a CSS class name to add to the row.
90 initComponent: function() {
94 me.selModel.view = me;
95 me.headerCt.view = me;
97 me.tpl = '<div></div>';
100 load: me.onStoreLoad,
107 // * @param {Ext.data.Record} record
108 // * @param {HTMLElement} row
109 // * @param {Number} rowIdx
115 // scroll to top of the grid when store loads
116 onStoreLoad: function(){
119 if (me.invalidateScrollerOnRefresh) {
121 if (!me.scrollToTopTask) {
122 me.scrollToTopTask = Ext.create('Ext.util.DelayedTask', me.scrollToTop, me);
124 me.scrollToTopTask.delay(1);
131 // scroll the view to the top
132 scrollToTop: Ext.emptyFn,
135 * Add a listener to the main view element. It will be destroyed with the view.
138 addElListener: function(eventName, fn, scope){
139 this.mon(this, eventName, fn, scope, {
145 * Get the columns used for generating a template via TableChunker.
146 * See {@link Ext.grid.header.Container#getGridColumns}.
149 getGridColumns: function() {
150 return this.headerCt.getGridColumns();
154 * Get a leaf level header by index regardless of what the nesting
157 * @param {Number} index The index
159 getHeaderAtIndex: function(index) {
160 return this.headerCt.getHeaderAtIndex(index);
164 * Get the cell (td) for a particular record and column.
165 * @param {Ext.data.Model} record
166 * @param {Ext.grid.column.Colunm} column
169 getCell: function(record, column) {
170 var row = this.getNode(record);
171 return Ext.fly(row).down(column.getCellSelector());
175 * Get a reference to a feature
176 * @param {String} id The id of the feature
177 * @return {Ext.grid.feature.Feature} The feature. Undefined if not found
179 getFeature: function(id) {
180 var features = this.featuresMC;
182 return features.get(id);
187 * Initializes each feature and bind it to this view.
190 initFeatures: function() {
196 me.features = me.features || [];
197 features = me.features;
198 len = features.length;
200 me.featuresMC = Ext.create('Ext.util.MixedCollection');
201 for (; i < len; i++) {
202 // ensure feature hasnt already been instantiated
203 if (!features[i].isFeature) {
204 features[i] = Ext.create('feature.' + features[i].ftype, features[i]);
206 // inject a reference to view
207 features[i].view = me;
208 me.featuresMC.add(features[i]);
213 * Gives features an injection point to attach events to the markup that
214 * has been created for this view.
217 attachEventsForFeatures: function() {
218 var features = this.features,
219 ln = features.length,
222 for (; i < ln; i++) {
223 if (features[i].isFeature) {
224 features[i].attachEvents();
229 afterRender: function() {
234 scroll: me.fireBodyScroll,
237 me.el.unselectable();
238 me.attachEventsForFeatures();
241 fireBodyScroll: function(e, t) {
242 this.fireEvent('bodyscroll', e, t);
245 // TODO: Refactor headerCt dependency here to colModel
247 * Uses the headerCt to transform data from dataIndex keys in a record to
248 * headerId keys in each header and then run them through each feature to
249 * get additional data for variables they have injected into the view template.
252 prepareData: function(data, idx, record) {
254 orig = me.headerCt.prepareData(data, idx, record, me, me.ownerCt),
255 features = me.features,
256 ln = features.length,
260 for (; i < ln; i++) {
261 feature = features[i];
262 if (feature.isFeature) {
263 Ext.apply(orig, feature.getAdditionalData(data, idx, record, orig, me));
270 // TODO: Refactor headerCt dependency here to colModel
271 collectData: function(records, startIndex) {
272 var preppedRecords = this.callParent(arguments),
273 headerCt = this.headerCt,
274 fullWidth = headerCt.getFullWidth(),
275 features = this.features,
276 ln = features.length,
278 rows: preppedRecords,
287 jln = preppedRecords.length;
288 // process row classes, rowParams has been deprecated and has been moved
289 // to the individual features that implement the behavior.
290 if (this.getRowClass) {
291 for (; j < jln; j++) {
293 preppedRecords[j]['rowCls'] = this.getRowClass(records[j], j, rowParams, this.store);
296 Ext.Error.raise("The getRowClass alt property is no longer supported.");
298 if (rowParams.tstyle) {
299 Ext.Error.raise("The getRowClass tstyle property is no longer supported.");
301 if (rowParams.cells) {
302 Ext.Error.raise("The getRowClass cells property is no longer supported.");
304 if (rowParams.body) {
305 Ext.Error.raise("The getRowClass body property is no longer supported. Use the getAdditionalData method of the rowbody feature.");
307 if (rowParams.bodyStyle) {
308 Ext.Error.raise("The getRowClass bodyStyle property is no longer supported.");
310 if (rowParams.cols) {
311 Ext.Error.raise("The getRowClass cols property is no longer supported.");
316 // currently only one feature may implement collectData. This is to modify
317 // what's returned to the view before its rendered
318 for (; i < ln; i++) {
319 feature = features[i];
320 if (feature.isFeature && feature.collectData && !feature.disabled) {
321 o = feature.collectData(records, preppedRecords, startIndex, fullWidth, o);
328 // TODO: Refactor header resizing to column resizing
330 * When a header is resized, setWidth on the individual columns resizer class,
331 * the top level table, save/restore scroll state, generate a new template and
332 * restore focus to the grid view's element so that keyboard navigation
336 onHeaderResize: function(header, w, suppressFocus) {
340 me.saveScrollState();
341 // Grab the col and set the width, css
342 // class is generated in TableChunker.
343 // Select composites because there may be several chunks.
344 el.select('.' + Ext.baseCSSPrefix + 'grid-col-resizer-'+header.id).setWidth(w);
345 el.select('.' + Ext.baseCSSPrefix + 'grid-table-resizer').setWidth(me.headerCt.getFullWidth());
346 me.restoreScrollState();
348 if (!suppressFocus) {
355 * When a header is shown restore its oldWidth if it was previously hidden.
358 onHeaderShow: function(headerCt, header, suppressFocus) {
359 // restore headers that were dynamically hidden
360 if (header.oldWidth) {
361 this.onHeaderResize(header, header.oldWidth, suppressFocus);
362 delete header.oldWidth;
363 // flexed headers will have a calculated size set
364 // this additional check has to do with the fact that
365 // defaults: {width: 100} will fight with a flex value
366 } else if (header.width && !header.flex) {
367 this.onHeaderResize(header, header.width, suppressFocus);
369 this.setNewTemplate();
373 * When the header hides treat it as a resize to 0.
376 onHeaderHide: function(headerCt, header, suppressFocus) {
377 this.onHeaderResize(header, 0, suppressFocus);
381 * Set a new template based on the current columns displayed in the
385 setNewTemplate: function() {
387 columns = me.headerCt.getColumnsForTpl(true);
389 me.tpl = me.getTableChunker().getTableTpl({
391 features: me.features
396 * Get the configured chunker or default of Ext.view.TableChunker
398 getTableChunker: function() {
399 return this.chunker || Ext.view.TableChunker;
403 * Add a CSS Class to a specific row.
404 * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model representing this row
405 * @param {String} cls
407 addRowCls: function(rowInfo, cls) {
408 var row = this.getNode(rowInfo);
410 Ext.fly(row).addCls(cls);
415 * Remove a CSS Class from a specific row.
416 * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model representing this row
417 * @param {String} cls
419 removeRowCls: function(rowInfo, cls) {
420 var row = this.getNode(rowInfo);
422 Ext.fly(row).removeCls(cls);
426 // GridSelectionModel invokes onRowSelect as selection changes
427 onRowSelect : function(rowIdx) {
428 this.addRowCls(rowIdx, this.selectedItemCls);
431 // GridSelectionModel invokes onRowDeselect as selection changes
432 onRowDeselect : function(rowIdx) {
435 me.removeRowCls(rowIdx, me.selectedItemCls);
436 me.removeRowCls(rowIdx, me.focusedItemCls);
439 onCellSelect: function(position) {
440 var cell = this.getCellByPosition(position);
442 cell.addCls(this.selectedCellCls);
446 onCellDeselect: function(position) {
447 var cell = this.getCellByPosition(position);
449 cell.removeCls(this.selectedCellCls);
454 onCellFocus: function(position) {
455 //var cell = this.getCellByPosition(position);
456 this.focusCell(position);
459 getCellByPosition: function(position) {
460 var row = position.row,
461 column = position.column,
463 node = this.getNode(row),
464 header = this.headerCt.getHeaderAtIndex(column),
468 if (header && node) {
469 cellSelector = header.getCellSelector();
470 cell = Ext.fly(node).down(cellSelector);
475 // GridSelectionModel invokes onRowFocus to 'highlight'
476 // the last row focused
477 onRowFocus: function(rowIdx, highlight, supressFocus) {
479 row = me.getNode(rowIdx);
482 me.addRowCls(rowIdx, me.focusedItemCls);
486 //this.el.dom.setAttribute('aria-activedescendant', row.id);
488 me.removeRowCls(rowIdx, me.focusedItemCls);
493 * Focus a particular row and bring it into view. Will fire the rowfocus event.
494 * @cfg {Mixed} An HTMLElement template node, index of a template node, the
495 * id of a template node or the record associated with the node.
497 focusRow: function(rowIdx) {
499 row = me.getNode(rowIdx),
508 elRegion = el.getRegion();
509 rowRegion = Ext.fly(row).getRegion();
511 if (rowRegion.top < elRegion.top) {
512 adjustment = rowRegion.top - elRegion.top;
514 } else if (rowRegion.bottom > elRegion.bottom) {
515 adjustment = rowRegion.bottom - elRegion.bottom;
517 record = me.getRecord(row);
518 rowIdx = me.store.indexOf(record);
521 // scroll the grid itself, so that all gridview's update.
522 panel.scrollByDeltaY(adjustment);
524 me.fireEvent('rowfocus', record, row, rowIdx);
528 focusCell: function(position) {
530 cell = me.getCellByPosition(position),
534 elRegion = el.getRegion(),
540 cellRegion = cell.getRegion();
542 if (cellRegion.top < elRegion.top) {
543 adjustmentY = cellRegion.top - elRegion.top;
545 } else if (cellRegion.bottom > elRegion.bottom) {
546 adjustmentY = cellRegion.bottom - elRegion.bottom;
550 if (cellRegion.left < elRegion.left) {
551 adjustmentX = cellRegion.left - elRegion.left;
553 } else if (cellRegion.right > elRegion.right) {
554 adjustmentX = cellRegion.right - elRegion.right;
558 // scroll the grid itself, so that all gridview's update.
559 panel.scrollByDeltaY(adjustmentY);
562 panel.scrollByDeltaX(adjustmentX);
565 me.fireEvent('cellfocus', record, cell, position);
570 * Scroll by delta. This affects this individual view ONLY and does not
571 * synchronize across views or scrollers.
572 * @param {Number} delta
573 * @param {String} dir (optional) Valid values are scrollTop and scrollLeft. Defaults to scrollTop.
576 scrollByDelta: function(delta, dir) {
577 dir = dir || 'scrollTop';
578 var elDom = this.el.dom;
579 elDom[dir] = (elDom[dir] += delta);
582 onUpdate: function(ds, index) {
583 this.callParent(arguments);
587 * Save the scrollState in a private variable.
588 * Must be used in conjunction with restoreScrollState
590 saveScrollState: function() {
591 var dom = this.el.dom,
592 state = this.scrollState;
594 state.left = dom.scrollLeft;
595 state.top = dom.scrollTop;
599 * Restore the scrollState.
600 * Must be used in conjunction with saveScrollState
603 restoreScrollState: function() {
604 var dom = this.el.dom,
605 state = this.scrollState,
606 headerEl = this.headerCt.el.dom;
608 headerEl.scrollLeft = dom.scrollLeft = state.left;
609 dom.scrollTop = state.top;
613 * Refresh the grid view.
614 * Saves and restores the scroll state, generates a new template, stripes rows
615 * and invalidates the scrollers.
616 * @param {Boolean} firstPass This is a private flag for internal use only.
618 refresh: function(firstPass) {
622 //this.saveScrollState();
625 me.callParent(arguments);
627 //this.restoreScrollState();
629 if (me.rendered && !firstPass) {
630 // give focus back to gridview
635 processItemEvent: function(record, row, rowIndex, e) {
637 cell = e.getTarget(me.cellSelector, row),
638 cellIndex = cell ? cell.cellIndex : -1,
639 map = me.statics().EventMap,
640 selModel = me.getSelectionModel(),
644 if (type == 'keydown' && !cell && selModel.getCurrentPosition) {
645 // CellModel, otherwise we can't tell which cell to invoke
646 cell = me.getCellByPosition(selModel.getCurrentPosition());
649 cellIndex = cell.cellIndex;
653 result = me.fireEvent('uievent', type, me, cell, rowIndex, cellIndex, e);
655 if (result === false || me.callParent(arguments) === false) {
659 // Don't handle cellmouseenter and cellmouseleave events for now
660 if (type == 'mouseover' || type == 'mouseout') {
665 // We are adding cell and feature events
666 (me['onBeforeCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
667 (me.fireEvent('beforecell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false) ||
668 (me['onCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
669 (me.fireEvent('cell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false)
673 processSpecialEvent: function(e) {
675 map = me.statics().EventMap,
676 features = me.features,
677 ln = features.length,
679 i, feature, prefix, featureTarget,
683 me.callParent(arguments);
685 if (type == 'mouseover' || type == 'mouseout') {
689 for (i = 0; i < ln; i++) {
690 feature = features[i];
691 if (feature.hasFeatureEvent) {
692 featureTarget = e.getTarget(feature.eventSelector, me.getTargetEl());
694 prefix = feature.eventPrefix;
695 // allows features to implement getFireEventArgs to change the
696 // fireEvent signature
697 beforeArgs = feature.getFireEventArgs('before' + prefix + type, me, featureTarget, e);
698 args = feature.getFireEventArgs(prefix + type, me, featureTarget, e);
702 (me.fireEvent.apply(me, beforeArgs) === false) ||
704 (panel.fireEvent.apply(panel, beforeArgs) === false) ||
706 (me.fireEvent.apply(me, args) === false) ||
708 (panel.fireEvent.apply(panel, args) === false)
718 onCellMouseDown: Ext.emptyFn,
719 onCellMouseUp: Ext.emptyFn,
720 onCellClick: Ext.emptyFn,
721 onCellDblClick: Ext.emptyFn,
722 onCellContextMenu: Ext.emptyFn,
723 onCellKeyDown: Ext.emptyFn,
724 onBeforeCellMouseDown: Ext.emptyFn,
725 onBeforeCellMouseUp: Ext.emptyFn,
726 onBeforeCellClick: Ext.emptyFn,
727 onBeforeCellDblClick: Ext.emptyFn,
728 onBeforeCellContextMenu: Ext.emptyFn,
729 onBeforeCellKeyDown: Ext.emptyFn,
732 * Expand a particular header to fit the max content width.
733 * This will ONLY expand, not contract.
736 expandToFit: function(header) {
737 var maxWidth = this.getMaxContentWidth(header);
739 header.setWidth(maxWidth);
743 * Get the max contentWidth of the header's text and all cells
744 * in the grid under this header.
747 getMaxContentWidth: function(header) {
748 var cellSelector = header.getCellInnerSelector(),
749 cells = this.el.query(cellSelector),
752 maxWidth = header.el.dom.scrollWidth,
755 for (; i < ln; i++) {
756 scrollWidth = cells[i].scrollWidth;
757 if (scrollWidth > maxWidth) {
758 maxWidth = scrollWidth;
764 getPositionByEvent: function(e) {
766 cellNode = e.getTarget(me.cellSelector),
767 rowNode = e.getTarget(me.itemSelector),
768 record = me.getRecord(rowNode),
769 header = me.getHeaderByCell(cellNode);
771 return me.getPosition(record, header);
774 getHeaderByCell: function(cell) {
776 var m = cell.className.match(this.cellRe);
778 return Ext.getCmp(m[1]);
785 * @param {Object} position The current row and column: an object containing the following properties:<ul>
786 * <li>row<div class="sub-desc"> The row <b>index</b></div></li>
787 * <li>column<div class="sub-desc">The column <b>index</b></div></li>
789 * @param {String} direction 'up', 'down', 'right' and 'left'
790 * @param {Ext.EventObject} e event
791 * @param {Boolean} preventWrap Set to true to prevent wrap around to the next or previous row.
792 * @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.
793 * @param {Scope} scope Scope to run the verifierFn in
794 * @returns {Object} newPosition An object containing the following properties:<ul>
795 * <li>row<div class="sub-desc"> The row <b>index</b></div></li>
796 * <li>column<div class="sub-desc">The column <b>index</b></div></li>
800 walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {
804 rowCount = me.store.getCount(),
805 firstCol = me.getFirstVisibleColumnIndex(),
806 lastCol = me.getLastVisibleColumnIndex(),
807 newPos = {row: row, column: column},
808 activeHeader = me.headerCt.getHeaderAtIndex(column);
810 // no active header or its currently hidden
811 if (!activeHeader || activeHeader.hidden) {
816 direction = direction.toLowerCase();
819 // has the potential to wrap if its last
820 if (column === lastCol) {
821 // if bottom row and last column, deny right
822 if (preventWrap || row === rowCount - 1) {
826 // otherwise wrap to nextRow and firstCol
827 newPos.row = row + 1;
828 newPos.column = firstCol;
833 newPos.column = column + me.getRightGap(activeHeader);
835 newPos.column = lastCol;
841 // has the potential to wrap
842 if (column === firstCol) {
843 // if top row and first column, deny left
844 if (preventWrap || row === 0) {
848 // otherwise wrap to prevRow and lastCol
849 newPos.row = row - 1;
850 newPos.column = lastCol;
855 newPos.column = column + me.getLeftGap(activeHeader);
857 newPos.column = firstCol;
863 // if top row, deny up
869 newPos.row = row - 1;
877 // if bottom row, deny down
878 if (row === rowCount - 1) {
883 newPos.row = row + 1;
885 newPos.row = rowCount - 1;
891 if (verifierFn && verifierFn.call(scope || window, newPos) !== true) {
897 getFirstVisibleColumnIndex: function() {
898 var headerCt = this.getHeaderCt(),
899 allColumns = headerCt.getGridColumns(),
900 visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
901 firstHeader = visHeaders[0];
903 return headerCt.getHeaderIndex(firstHeader);
906 getLastVisibleColumnIndex: function() {
907 var headerCt = this.getHeaderCt(),
908 allColumns = headerCt.getGridColumns(),
909 visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
910 lastHeader = visHeaders[visHeaders.length - 1];
912 return headerCt.getHeaderIndex(lastHeader);
915 getHeaderCt: function() {
916 return this.headerCt;
919 getPosition: function(record, header) {
922 gridCols = me.headerCt.getGridColumns();
925 row: store.indexOf(record),
926 column: Ext.Array.indexOf(gridCols, header)
931 * Determines the 'gap' between the closest adjacent header to the right
932 * that is not hidden.
935 getRightGap: function(activeHeader) {
936 var headerCt = this.getHeaderCt(),
937 headers = headerCt.getGridColumns(),
938 activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
939 i = activeHeaderIdx + 1,
942 for (; i <= headers.length; i++) {
943 if (!headers[i].hidden) {
949 return nextIdx - activeHeaderIdx;
952 beforeDestroy: function() {
954 this.el.removeAllListeners();
956 this.callParent(arguments);
960 * Determines the 'gap' between the closest adjacent header to the left
961 * that is not hidden.
964 getLeftGap: function(activeHeader) {
965 var headerCt = this.getHeaderCt(),
966 headers = headerCt.getGridColumns(),
967 activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
968 i = activeHeaderIdx - 1,
971 for (; i >= 0; i--) {
972 if (!headers[i].hidden) {
978 return prevIdx - activeHeaderIdx;