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 * This class encapsulates the user interface for a tabular data set.
17 * It acts as a centralized manager for controlling the various interface
18 * elements of the view. This includes handling events, such as row and cell
19 * level based DOM events. It also reacts to events from the underlying {@link Ext.selection.Model}
20 * to provide visual feedback to the user.
22 * This class does not provide ways to manipulate the underlying data of the configured
23 * {@link Ext.data.Store}.
25 * This is the base class for both {@link Ext.grid.View} and {@link Ext.tree.View} and is not
26 * to be used directly.
28 Ext.define('Ext.view.Table', {
29 extend: 'Ext.view.View',
30 alias: 'widget.tableview',
32 'Ext.view.TableChunker',
33 'Ext.util.DelayedTask',
34 'Ext.util.MixedCollection'
37 baseCls: Ext.baseCSSPrefix + 'grid-view',
40 itemSelector: '.' + Ext.baseCSSPrefix + 'grid-row',
42 cellSelector: '.' + Ext.baseCSSPrefix + 'grid-cell',
44 selectedItemCls: Ext.baseCSSPrefix + 'grid-row-selected',
45 selectedCellCls: Ext.baseCSSPrefix + 'grid-cell-selected',
46 focusedItemCls: Ext.baseCSSPrefix + 'grid-row-focused',
47 overItemCls: Ext.baseCSSPrefix + 'grid-row-over',
48 altRowCls: Ext.baseCSSPrefix + 'grid-row-alt',
49 rowClsRe: /(?:^|\s*)grid-row-(first|last|alt)(?:\s+|$)/g,
50 cellRe: new RegExp('x-grid-cell-([^\\s]+) ', ''),
56 * Override this function to apply custom CSS classes to rows during rendering. This function should return the
57 * CSS class name (or empty string '' for none) that will be added to the row's wrapping div. To apply multiple
58 * class names, simply return them space-delimited within the string (e.g. 'my-class another-class').
62 * getRowClass: function(record, rowIndex, rowParams, store){
63 * return record.get("valid") ? "row-valid" : "row-error";
67 * @param {Ext.data.Model} record The record corresponding to the current row.
68 * @param {Number} index The row index.
69 * @param {Object} rowParams **DEPRECATED.** For row body use the
70 * {@link Ext.grid.feature.RowBody#getAdditionalData getAdditionalData} method of the rowbody feature.
71 * @param {Ext.data.Store} store The store this grid is bound to
72 * @return {String} a CSS class name to add to the row.
77 initComponent: function() {
81 me.selModel.view = me;
82 me.headerCt.view = me;
84 me.tpl = '<div></div>';
94 // * @param {Ext.data.Model} record
95 // * @param {HTMLElement} row
96 // * @param {Number} rowIdx
102 // scroll to top of the grid when store loads
103 onStoreLoad: function(){
106 if (me.invalidateScrollerOnRefresh) {
108 if (!me.scrollToTopTask) {
109 me.scrollToTopTask = Ext.create('Ext.util.DelayedTask', me.scrollToTop, me);
111 me.scrollToTopTask.delay(1);
118 // scroll the view to the top
119 scrollToTop: Ext.emptyFn,
122 * Add a listener to the main view element. It will be destroyed with the view.
125 addElListener: function(eventName, fn, scope){
126 this.mon(this, eventName, fn, scope, {
132 * Get the columns used for generating a template via TableChunker.
133 * See {@link Ext.grid.header.Container#getGridColumns}.
136 getGridColumns: function() {
137 return this.headerCt.getGridColumns();
141 * Get a leaf level header by index regardless of what the nesting
144 * @param {Number} index The index
146 getHeaderAtIndex: function(index) {
147 return this.headerCt.getHeaderAtIndex(index);
151 * Get the cell (td) for a particular record and column.
152 * @param {Ext.data.Model} record
153 * @param {Ext.grid.column.Column} column
156 getCell: function(record, column) {
157 var row = this.getNode(record);
158 return Ext.fly(row).down(column.getCellSelector());
162 * Get a reference to a feature
163 * @param {String} id The id of the feature
164 * @return {Ext.grid.feature.Feature} The feature. Undefined if not found
166 getFeature: function(id) {
167 var features = this.featuresMC;
169 return features.get(id);
174 * Initializes each feature and bind it to this view.
177 initFeatures: function() {
183 me.features = me.features || [];
184 features = me.features;
185 len = features.length;
187 me.featuresMC = Ext.create('Ext.util.MixedCollection');
188 for (; i < len; i++) {
189 // ensure feature hasnt already been instantiated
190 if (!features[i].isFeature) {
191 features[i] = Ext.create('feature.' + features[i].ftype, features[i]);
193 // inject a reference to view
194 features[i].view = me;
195 me.featuresMC.add(features[i]);
200 * Gives features an injection point to attach events to the markup that
201 * has been created for this view.
204 attachEventsForFeatures: function() {
205 var features = this.features,
206 ln = features.length,
209 for (; i < ln; i++) {
210 if (features[i].isFeature) {
211 features[i].attachEvents();
216 afterRender: function() {
221 scroll: me.fireBodyScroll,
224 me.el.unselectable();
225 me.attachEventsForFeatures();
228 fireBodyScroll: function(e, t) {
229 this.fireEvent('bodyscroll', e, t);
232 // TODO: Refactor headerCt dependency here to colModel
234 * Uses the headerCt to transform data from dataIndex keys in a record to
235 * headerId keys in each header and then run them through each feature to
236 * get additional data for variables they have injected into the view template.
239 prepareData: function(data, idx, record) {
241 orig = me.headerCt.prepareData(data, idx, record, me, me.ownerCt),
242 features = me.features,
243 ln = features.length,
247 for (; i < ln; i++) {
248 feature = features[i];
249 if (feature.isFeature) {
250 Ext.apply(orig, feature.getAdditionalData(data, idx, record, orig, me));
257 // TODO: Refactor headerCt dependency here to colModel
258 collectData: function(records, startIndex) {
259 var preppedRecords = this.callParent(arguments),
260 headerCt = this.headerCt,
261 fullWidth = headerCt.getFullWidth(),
262 features = this.features,
263 ln = features.length,
265 rows: preppedRecords,
274 jln = preppedRecords.length;
275 // process row classes, rowParams has been deprecated and has been moved
276 // to the individual features that implement the behavior.
277 if (this.getRowClass) {
278 for (; j < jln; j++) {
280 preppedRecords[j]['rowCls'] = this.getRowClass(records[j], j, rowParams, this.store);
283 Ext.Error.raise("The getRowClass alt property is no longer supported.");
285 if (rowParams.tstyle) {
286 Ext.Error.raise("The getRowClass tstyle property is no longer supported.");
288 if (rowParams.cells) {
289 Ext.Error.raise("The getRowClass cells property is no longer supported.");
291 if (rowParams.body) {
292 Ext.Error.raise("The getRowClass body property is no longer supported. Use the getAdditionalData method of the rowbody feature.");
294 if (rowParams.bodyStyle) {
295 Ext.Error.raise("The getRowClass bodyStyle property is no longer supported.");
297 if (rowParams.cols) {
298 Ext.Error.raise("The getRowClass cols property is no longer supported.");
303 // currently only one feature may implement collectData. This is to modify
304 // what's returned to the view before its rendered
305 for (; i < ln; i++) {
306 feature = features[i];
307 if (feature.isFeature && feature.collectData && !feature.disabled) {
308 o = feature.collectData(records, preppedRecords, startIndex, fullWidth, o);
315 // TODO: Refactor header resizing to column resizing
317 * When a header is resized, setWidth on the individual columns resizer class,
318 * the top level table, save/restore scroll state, generate a new template and
319 * restore focus to the grid view's element so that keyboard navigation
323 onHeaderResize: function(header, w, suppressFocus) {
328 me.saveScrollState();
329 // Grab the col and set the width, css
330 // class is generated in TableChunker.
331 // Select composites because there may be several chunks.
334 // Setting the width of the first TD does not work - ends up with a 1 pixel discrepancy.
335 // We need to increment the passed with in this case.
336 if (Ext.isIE6 || Ext.isIE7) {
337 if (header.el.hasCls(Ext.baseCSSPrefix + 'column-header-first')) {
341 el.select('.' + Ext.baseCSSPrefix + 'grid-col-resizer-'+header.id).setWidth(w);
342 el.select('.' + Ext.baseCSSPrefix + 'grid-table-resizer').setWidth(me.headerCt.getFullWidth());
343 me.restoreScrollState();
344 if (!me.ignoreTemplate) {
347 if (!suppressFocus) {
354 * When a header is shown restore its oldWidth if it was previously hidden.
357 onHeaderShow: function(headerCt, header, suppressFocus) {
359 me.ignoreTemplate = true;
360 // restore headers that were dynamically hidden
361 if (header.oldWidth) {
362 me.onHeaderResize(header, header.oldWidth, suppressFocus);
363 delete header.oldWidth;
364 // flexed headers will have a calculated size set
365 // this additional check has to do with the fact that
366 // defaults: {width: 100} will fight with a flex value
367 } else if (header.width && !header.flex) {
368 me.onHeaderResize(header, header.width, suppressFocus);
370 delete me.ignoreTemplate;
375 * When the header hides treat it as a resize to 0.
378 onHeaderHide: function(headerCt, header, suppressFocus) {
379 this.onHeaderResize(header, 0, suppressFocus);
383 * Set a new template based on the current columns displayed in the
387 setNewTemplate: function() {
389 columns = me.headerCt.getColumnsForTpl(true);
391 me.tpl = me.getTableChunker().getTableTpl({
393 features: me.features
398 * Returns the configured chunker or default of Ext.view.TableChunker
400 getTableChunker: function() {
401 return this.chunker || Ext.view.TableChunker;
405 * Adds a CSS Class to a specific row.
406 * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model
407 * representing this row
408 * @param {String} cls
410 addRowCls: function(rowInfo, cls) {
411 var row = this.getNode(rowInfo);
413 Ext.fly(row).addCls(cls);
418 * Removes a CSS Class from a specific row.
419 * @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model
420 * representing this row
421 * @param {String} cls
423 removeRowCls: function(rowInfo, cls) {
424 var row = this.getNode(rowInfo);
426 Ext.fly(row).removeCls(cls);
430 // GridSelectionModel invokes onRowSelect as selection changes
431 onRowSelect : function(rowIdx) {
432 this.addRowCls(rowIdx, this.selectedItemCls);
435 // GridSelectionModel invokes onRowDeselect as selection changes
436 onRowDeselect : function(rowIdx) {
439 me.removeRowCls(rowIdx, me.selectedItemCls);
440 me.removeRowCls(rowIdx, me.focusedItemCls);
443 onCellSelect: function(position) {
444 var cell = this.getCellByPosition(position);
446 cell.addCls(this.selectedCellCls);
450 onCellDeselect: function(position) {
451 var cell = this.getCellByPosition(position);
453 cell.removeCls(this.selectedCellCls);
458 onCellFocus: function(position) {
459 //var cell = this.getCellByPosition(position);
460 this.focusCell(position);
463 getCellByPosition: function(position) {
464 var row = position.row,
465 column = position.column,
467 node = this.getNode(row),
468 header = this.headerCt.getHeaderAtIndex(column),
472 if (header && node) {
473 cellSelector = header.getCellSelector();
474 cell = Ext.fly(node).down(cellSelector);
479 // GridSelectionModel invokes onRowFocus to 'highlight'
480 // the last row focused
481 onRowFocus: function(rowIdx, highlight, supressFocus) {
483 row = me.getNode(rowIdx);
486 me.addRowCls(rowIdx, me.focusedItemCls);
490 //this.el.dom.setAttribute('aria-activedescendant', row.id);
492 me.removeRowCls(rowIdx, me.focusedItemCls);
497 * Focuses a particular row and brings it into view. Will fire the rowfocus event.
498 * @param {HTMLElement/String/Number/Ext.data.Model} rowIdx
499 * An HTMLElement template node, index of a template node, the id of a template node or the
500 * record associated with the node.
502 focusRow: function(rowIdx) {
504 row = me.getNode(rowIdx),
513 elRegion = el.getRegion();
514 rowRegion = Ext.fly(row).getRegion();
516 if (rowRegion.top < elRegion.top) {
517 adjustment = rowRegion.top - elRegion.top;
519 } else if (rowRegion.bottom > elRegion.bottom) {
520 adjustment = rowRegion.bottom - elRegion.bottom;
522 record = me.getRecord(row);
523 rowIdx = me.store.indexOf(record);
526 // scroll the grid itself, so that all gridview's update.
527 panel.scrollByDeltaY(adjustment);
529 me.fireEvent('rowfocus', record, row, rowIdx);
533 focusCell: function(position) {
535 cell = me.getCellByPosition(position),
539 elRegion = el.getRegion(),
545 cellRegion = cell.getRegion();
547 if (cellRegion.top < elRegion.top) {
548 adjustmentY = cellRegion.top - elRegion.top;
550 } else if (cellRegion.bottom > elRegion.bottom) {
551 adjustmentY = cellRegion.bottom - elRegion.bottom;
555 if (cellRegion.left < elRegion.left) {
556 adjustmentX = cellRegion.left - elRegion.left;
558 } else if (cellRegion.right > elRegion.right) {
559 adjustmentX = cellRegion.right - elRegion.right;
563 // scroll the grid itself, so that all gridview's update.
564 panel.scrollByDeltaY(adjustmentY);
567 panel.scrollByDeltaX(adjustmentX);
570 me.fireEvent('cellfocus', record, cell, position);
575 * Scrolls by delta. This affects this individual view ONLY and does not
576 * synchronize across views or scrollers.
577 * @param {Number} delta
578 * @param {String} dir (optional) Valid values are scrollTop and scrollLeft. Defaults to scrollTop.
581 scrollByDelta: function(delta, dir) {
582 dir = dir || 'scrollTop';
583 var elDom = this.el.dom;
584 elDom[dir] = (elDom[dir] += delta);
587 onUpdate: function(ds, index) {
588 this.callParent(arguments);
592 * Saves the scrollState in a private variable. Must be used in conjunction with restoreScrollState
594 saveScrollState: function() {
596 var dom = this.el.dom,
597 state = this.scrollState;
599 state.left = dom.scrollLeft;
600 state.top = dom.scrollTop;
605 * Restores the scrollState.
606 * Must be used in conjunction with saveScrollState
609 restoreScrollState: function() {
611 var dom = this.el.dom,
612 state = this.scrollState,
613 headerEl = this.headerCt.el.dom;
615 headerEl.scrollLeft = dom.scrollLeft = state.left;
616 dom.scrollTop = state.top;
621 * Refreshes the grid view. Saves and restores the scroll state, generates a new template, stripes rows and
622 * invalidates the scrollers.
624 refresh: function() {
625 this.setNewTemplate();
626 this.callParent(arguments);
629 processItemEvent: function(record, row, rowIndex, e) {
631 cell = e.getTarget(me.cellSelector, row),
632 cellIndex = cell ? cell.cellIndex : -1,
633 map = me.statics().EventMap,
634 selModel = me.getSelectionModel(),
638 if (type == 'keydown' && !cell && selModel.getCurrentPosition) {
639 // CellModel, otherwise we can't tell which cell to invoke
640 cell = me.getCellByPosition(selModel.getCurrentPosition());
643 cellIndex = cell.cellIndex;
647 result = me.fireEvent('uievent', type, me, cell, rowIndex, cellIndex, e);
649 if (result === false || me.callParent(arguments) === false) {
653 // Don't handle cellmouseenter and cellmouseleave events for now
654 if (type == 'mouseover' || type == 'mouseout') {
659 // We are adding cell and feature events
660 (me['onBeforeCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
661 (me.fireEvent('beforecell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false) ||
662 (me['onCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
663 (me.fireEvent('cell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false)
667 processSpecialEvent: function(e) {
669 map = me.statics().EventMap,
670 features = me.features,
671 ln = features.length,
673 i, feature, prefix, featureTarget,
677 me.callParent(arguments);
679 if (type == 'mouseover' || type == 'mouseout') {
683 for (i = 0; i < ln; i++) {
684 feature = features[i];
685 if (feature.hasFeatureEvent) {
686 featureTarget = e.getTarget(feature.eventSelector, me.getTargetEl());
688 prefix = feature.eventPrefix;
689 // allows features to implement getFireEventArgs to change the
690 // fireEvent signature
691 beforeArgs = feature.getFireEventArgs('before' + prefix + type, me, featureTarget, e);
692 args = feature.getFireEventArgs(prefix + type, me, featureTarget, e);
696 (me.fireEvent.apply(me, beforeArgs) === false) ||
698 (panel.fireEvent.apply(panel, beforeArgs) === false) ||
700 (me.fireEvent.apply(me, args) === false) ||
702 (panel.fireEvent.apply(panel, args) === false)
712 onCellMouseDown: Ext.emptyFn,
713 onCellMouseUp: Ext.emptyFn,
714 onCellClick: Ext.emptyFn,
715 onCellDblClick: Ext.emptyFn,
716 onCellContextMenu: Ext.emptyFn,
717 onCellKeyDown: Ext.emptyFn,
718 onBeforeCellMouseDown: Ext.emptyFn,
719 onBeforeCellMouseUp: Ext.emptyFn,
720 onBeforeCellClick: Ext.emptyFn,
721 onBeforeCellDblClick: Ext.emptyFn,
722 onBeforeCellContextMenu: Ext.emptyFn,
723 onBeforeCellKeyDown: Ext.emptyFn,
726 * Expands a particular header to fit the max content width.
727 * This will ONLY expand, not contract.
730 expandToFit: function(header) {
732 var maxWidth = this.getMaxContentWidth(header);
734 header.setWidth(maxWidth);
739 * Returns the max contentWidth of the header's text and all cells
740 * in the grid under this header.
743 getMaxContentWidth: function(header) {
744 var cellSelector = header.getCellInnerSelector(),
745 cells = this.el.query(cellSelector),
748 maxWidth = header.el.dom.scrollWidth,
751 for (; i < ln; i++) {
752 scrollWidth = cells[i].scrollWidth;
753 if (scrollWidth > maxWidth) {
754 maxWidth = scrollWidth;
760 getPositionByEvent: function(e) {
762 cellNode = e.getTarget(me.cellSelector),
763 rowNode = e.getTarget(me.itemSelector),
764 record = me.getRecord(rowNode),
765 header = me.getHeaderByCell(cellNode);
767 return me.getPosition(record, header);
770 getHeaderByCell: function(cell) {
772 var m = cell.className.match(this.cellRe);
774 return Ext.getCmp(m[1]);
781 * @param {Object} position The current row and column: an object containing the following properties:
783 * - row - The row index
784 * - column - The column index
786 * @param {String} direction 'up', 'down', 'right' and 'left'
787 * @param {Ext.EventObject} e event
788 * @param {Boolean} preventWrap Set to true to prevent wrap around to the next or previous row.
789 * @param {Function} verifierFn A function to verify the validity of the calculated position.
790 * When using this function, you must return true to allow the newPosition to be returned.
791 * @param {Object} scope Scope to run the verifierFn in
792 * @returns {Object} newPosition An object containing the following properties:
794 * - row - The row index
795 * - column - The column index
799 walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {
803 rowCount = me.store.getCount(),
804 firstCol = me.getFirstVisibleColumnIndex(),
805 lastCol = me.getLastVisibleColumnIndex(),
806 newPos = {row: row, column: column},
807 activeHeader = me.headerCt.getHeaderAtIndex(column);
809 // no active header or its currently hidden
810 if (!activeHeader || activeHeader.hidden) {
815 direction = direction.toLowerCase();
818 // has the potential to wrap if its last
819 if (column === lastCol) {
820 // if bottom row and last column, deny right
821 if (preventWrap || row === rowCount - 1) {
825 // otherwise wrap to nextRow and firstCol
826 newPos.row = row + 1;
827 newPos.column = firstCol;
832 newPos.column = column + me.getRightGap(activeHeader);
834 newPos.column = lastCol;
840 // has the potential to wrap
841 if (column === firstCol) {
842 // if top row and first column, deny left
843 if (preventWrap || row === 0) {
847 // otherwise wrap to prevRow and lastCol
848 newPos.row = row - 1;
849 newPos.column = lastCol;
854 newPos.column = column + me.getLeftGap(activeHeader);
856 newPos.column = firstCol;
862 // if top row, deny up
868 newPos.row = row - 1;
876 // if bottom row, deny down
877 if (row === rowCount - 1) {
882 newPos.row = row + 1;
884 newPos.row = rowCount - 1;
890 if (verifierFn && verifierFn.call(scope || window, newPos) !== true) {
896 getFirstVisibleColumnIndex: function() {
897 var headerCt = this.getHeaderCt(),
898 allColumns = headerCt.getGridColumns(),
899 visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
900 firstHeader = visHeaders[0];
902 return headerCt.getHeaderIndex(firstHeader);
905 getLastVisibleColumnIndex: function() {
906 var headerCt = this.getHeaderCt(),
907 allColumns = headerCt.getGridColumns(),
908 visHeaders = Ext.ComponentQuery.query(':not([hidden])', allColumns),
909 lastHeader = visHeaders[visHeaders.length - 1];
911 return headerCt.getHeaderIndex(lastHeader);
914 getHeaderCt: function() {
915 return this.headerCt;
918 getPosition: function(record, header) {
921 gridCols = me.headerCt.getGridColumns();
924 row: store.indexOf(record),
925 column: Ext.Array.indexOf(gridCols, header)
930 * Determines the 'gap' between the closest adjacent header to the right
931 * that is not hidden.
934 getRightGap: function(activeHeader) {
935 var headerCt = this.getHeaderCt(),
936 headers = headerCt.getGridColumns(),
937 activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
938 i = activeHeaderIdx + 1,
941 for (; i <= headers.length; i++) {
942 if (!headers[i].hidden) {
948 return nextIdx - activeHeaderIdx;
951 beforeDestroy: function() {
953 this.el.removeAllListeners();
955 this.callParent(arguments);
959 * Determines the 'gap' between the closest adjacent header to the left
960 * that is not hidden.
963 getLeftGap: function(activeHeader) {
964 var headerCt = this.getHeaderCt(),
965 headers = headerCt.getGridColumns(),
966 activeHeaderIdx = Ext.Array.indexOf(headers, activeHeader),
967 i = activeHeaderIdx - 1,
970 for (; i >= 0; i--) {
971 if (!headers[i].hidden) {
977 return prevIdx - activeHeaderIdx;