Форк Rambox
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

3089 lines
114 KiB

/**
* This class encapsulates the user interface for a tabular data set.
* It acts as a centralized manager for controlling the various interface
* elements of the view. This includes handling events, such as row and cell
* level based DOM events. It also reacts to events from the underlying {@link Ext.selection.Model}
* to provide visual feedback to the user.
*
* This class does not provide ways to manipulate the underlying data of the configured
* {@link Ext.data.Store}.
*
* This is the base class for both {@link Ext.grid.View} and {@link Ext.tree.View} and is not
* to be used directly.
*/
Ext.define('Ext.view.Table', {
extend: 'Ext.view.View',
xtype: [ 'tableview', 'gridview' ],
alternateClassName: 'Ext.grid.View',
requires: [
'Ext.grid.CellContext',
'Ext.view.TableLayout',
'Ext.grid.locking.RowSynchronizer',
'Ext.view.NodeCache',
'Ext.util.DelayedTask',
'Ext.util.MixedCollection'
],
/*
* @property {Boolean}
* `true` in this class to identify an object as an instantiated Ext.view.TableView, or subclass thereof.
*/
isTableView: true,
config: {
selectionModel: {
type: 'rowmodel'
}
},
inheritableStatics: {
// Events a TableView may fire.
// Used by Ext.grid.locking.View to relay view events to user code
events: [
"blur",
"focus",
"move",
"resize",
"destroy",
"beforedestroy",
"boxready",
"afterrender",
"render",
"beforerender",
"removed",
"hide",
"beforehide",
"show",
"beforeshow",
"enable",
"disable",
"added",
"deactivate",
"beforedeactivate",
"activate",
"beforeactivate",
"cellkeydown",
"beforecellkeydown",
"cellmouseup",
"beforecellmouseup",
"cellmousedown",
"beforecellmousedown",
"cellcontextmenu",
"beforecellcontextmenu",
"celldblclick",
"beforecelldblclick",
"cellclick",
"beforecellclick",
"refresh",
"itemremove",
"itemadd",
"itemupdate",
"viewready",
"beforerefresh",
"unhighlightitem",
"highlightitem",
"focuschange",
"deselect",
"select",
"beforedeselect",
"beforeselect",
"selectionchange",
"containerkeydown",
"containercontextmenu",
"containerdblclick",
"containerclick",
"containermouseout",
"containermouseover",
"containermouseup",
"containermousedown",
"beforecontainerkeydown",
"beforecontainercontextmenu",
"beforecontainerdblclick",
"beforecontainerclick",
"beforecontainermouseout",
"beforecontainermouseover",
"beforecontainermouseup",
"beforecontainermousedown",
"itemkeydown",
"itemcontextmenu",
"itemdblclick",
"itemclick",
"itemmouseleave",
"itemmouseenter",
"itemmouseup",
"itemmousedown",
"rowclick",
"rowcontextmenu",
"rowdblclick",
"rowkeydown",
"rowmouseup",
"rowmousedown",
"rowkeydown",
"beforeitemkeydown",
"beforeitemcontextmenu",
"beforeitemdblclick",
"beforeitemclick",
"beforeitemmouseleave",
"beforeitemmouseenter",
"beforeitemmouseup",
"beforeitemmousedown",
"statesave",
"beforestatesave",
"staterestore",
"beforestaterestore",
"uievent",
"groupcollapse",
"groupexpand"
]
},
scrollable: true,
componentLayout: 'tableview',
baseCls: Ext.baseCSSPrefix + 'grid-view',
unselectableCls: Ext.baseCSSPrefix + 'unselectable',
/**
* @cfg {String} [firstCls='x-grid-cell-first']
* A CSS class to add to the *first* cell in every row to enable special styling for the first column.
* If no styling is needed on the first column, this may be configured as `null`.
*/
firstCls: Ext.baseCSSPrefix + 'grid-cell-first',
/**
* @cfg {String} [lastCls='x-grid-cell-last']
* A CSS class to add to the *last* cell in every row to enable special styling for the last column.
* If no styling is needed on the last column, this may be configured as `null`.
*/
lastCls: Ext.baseCSSPrefix + 'grid-cell-last',
itemCls: Ext.baseCSSPrefix + 'grid-item',
selectedItemCls: Ext.baseCSSPrefix + 'grid-item-selected',
selectedCellCls: Ext.baseCSSPrefix + 'grid-cell-selected',
focusedItemCls: Ext.baseCSSPrefix + 'grid-item-focused',
overItemCls: Ext.baseCSSPrefix + 'grid-item-over',
altRowCls: Ext.baseCSSPrefix + 'grid-item-alt',
dirtyCls: Ext.baseCSSPrefix + 'grid-dirty-cell',
rowClsRe: new RegExp('(?:^|\\s*)' + Ext.baseCSSPrefix + 'grid-item-alt(?:\\s+|$)', 'g'),
cellRe: new RegExp(Ext.baseCSSPrefix + 'grid-cell-([^\\s]+)(?:\\s|$)', ''),
positionBody: true,
positionCells: false,
stripeOnUpdate: null,
// cfg docs inherited
trackOver: true,
/**
* Override this function to apply custom CSS classes to rows during rendering. This function should return the
* CSS class name (or empty string '' for none) that will be added to the row's wrapping element. To apply multiple
* class names, simply return them space-delimited within the string (e.g. 'my-class another-class').
* Example usage:
*
* viewConfig: {
* getRowClass: function(record, rowIndex, rowParams, store){
* return record.get("valid") ? "row-valid" : "row-error";
* }
* }
*
* @param {Ext.data.Model} record The record corresponding to the current row.
* @param {Number} index The row index.
* @param {Object} rowParams **DEPRECATED.** For row body use the
* {@link Ext.grid.feature.RowBody#getAdditionalData getAdditionalData} method of the rowbody feature.
* @param {Ext.data.Store} store The store this grid is bound to
* @return {String} a CSS class name to add to the row.
* @method
*/
getRowClass: null,
/**
* @cfg {Boolean} stripeRows
* True to stripe the rows.
*
* This causes the CSS class **`x-grid-row-alt`** to be added to alternate rows of
* the grid. A default CSS rule is provided which sets a background color, but you can override this
* with a rule which either overrides the **background-color** style using the `!important`
* modifier, or which uses a CSS selector of higher specificity.
*/
stripeRows: true,
/**
* @cfg {Boolean} markDirty
* True to show the dirty cell indicator when a cell has been modified.
*/
markDirty : true,
/**
* @cfg {Boolean} enableTextSelection
* True to enable text selections.
*/
ariaRole: 'grid',
/**
* @property {Ext.view.Table} ownerGrid
* A reference to the top-level owning grid component. This is actually the TablePanel
* so it could be a tree.
* @readonly
* @private
* @since 5.0.0
*/
/**
* @method disable
* Disable this view.
*
* Disables interaction with, and masks this view.
*
* Note that the encapsulating {@link Ext.panel.Table} panel is *not* disabled, and other *docked*
* components such as the panel header, the column header container, and docked toolbars will still be enabled.
* The panel itself can be disabled if that is required, or individual docked components could be disabled.
*
* See {@link Ext.panel.Table #disableColumnHeaders disableColumnHeaders} and {@link Ext.panel.Table #enableColumnHeaders enableColumnHeaders}.
*
* @param {Boolean} [silent=false] Passing `true` will suppress the `disable` event from being fired.
* @since 1.1.0
*/
/**
* @private
* Outer tpl for TableView just to satisfy the validation within AbstractView.initComponent.
*/
tpl: [
'{%',
'view = values.view;',
'if (!(columns = values.columns)) {',
'columns = values.columns = view.ownerCt.getVisibleColumnManager().getColumns();',
'}',
'values.fullWidth = 0;',
// Stamp cellWidth into the columns
'for (i = 0, len = columns.length; i < len; i++) {',
'column = columns[i];',
'values.fullWidth += (column.cellWidth = column.lastBox ? column.lastBox.width : column.width || column.minWidth);',
'}',
// Add the row/column line classes to the container element.
'tableCls=values.tableCls=[];',
'%}',
'<div class="' + Ext.baseCSSPrefix + 'grid-item-container" style="width:{fullWidth}px">',
'{[view.renderTHead(values, out, parent)]}',
'{%',
'view.renderRows(values.rows, values.columns, values.viewStartIndex, out);',
'%}',
'{[view.renderTFoot(values, out, parent)]}',
'</div>',
{
definitions: 'var view, tableCls, columns, i, len, column;',
priority: 0
}
],
outerRowTpl: [
'<table id="{rowId}" ',
'data-boundView="{view.id}" ',
'data-recordId="{record.internalId}" ',
'data-recordIndex="{recordIndex}" ',
'class="{[values.itemClasses.join(" ")]}" cellPadding="0" cellSpacing="0" {ariaTableAttr} style="{itemStyle};width:0">',
// Do NOT emit a <TBODY> tag in case the nextTpl has to emit a <COLGROUP> column sizer element.
// Browser will create a tbody tag when it encounters the first <TR>
'{%',
'this.nextTpl.applyOut(values, out, parent)',
'%}',
'</table>', {
priority: 9999
}
],
rowTpl: [
'{%',
'var dataRowCls = values.recordIndex === -1 ? "" : " ' + Ext.baseCSSPrefix + 'grid-row";',
'%}',
'<tr class="{[values.rowClasses.join(" ")]} {[dataRowCls]}" {rowAttr:attributes} {ariaRowAttr}>',
'<tpl for="columns">' +
'{%',
'parent.view.renderCell(values, parent.record, parent.recordIndex, parent.rowIndex, xindex - 1, out, parent)',
'%}',
'</tpl>',
'</tr>',
{
priority: 0
}
],
cellTpl: [
'<td class="{tdCls}" {tdAttr} {[Ext.aria ? "id=\\"" + Ext.id() + "\\"" : ""]} style="width:{column.cellWidth}px;<tpl if="tdStyle">{tdStyle}</tpl>" tabindex="-1" {ariaCellAttr} data-columnid="{[values.column.getItemId()]}">',
'<div {unselectableAttr} class="' + Ext.baseCSSPrefix + 'grid-cell-inner {innerCls}" ',
'style="text-align:{align};<tpl if="style">{style}</tpl>" {ariaCellInnerAttr}>{value}</div>',
'</td>', {
priority: 0
}
],
/**
* @private
* Flag to disable refreshing SelectionModel on view refresh. Table views render rows with selected CSS class already added if necessary.
*/
refreshSelmodelOnRefresh: false,
tableValues: {},
// Private properties used during the row and cell render process.
// They are allocated here on the prototype, and cleared/re-used to avoid GC churn during repeated rendering.
rowValues: {
itemClasses: [],
rowClasses: []
},
cellValues: {
classes: [
Ext.baseCSSPrefix + 'grid-cell ' + Ext.baseCSSPrefix + 'grid-td' // for styles shared between cell and rowwrap
]
},
/**
* @event beforecellclick
* Fired before the cell click is processed. Return false to cancel the default action.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event cellclick
* Fired when table cell is clicked.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event beforecelldblclick
* Fired before the cell double click is processed. Return false to cancel the default action.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event celldblclick
* Fired when table cell is double clicked.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event beforecellcontextmenu
* Fired before the cell right click is processed. Return false to cancel the default action.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event cellcontextmenu
* Fired when table cell is right clicked.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event beforecellmousedown
* Fired before the cell mouse down is processed. Return false to cancel the default action.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event cellmousedown
* Fired when the mousedown event is captured on the cell.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event beforecellmouseup
* Fired before the cell mouse up is processed. Return false to cancel the default action.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event cellmouseup
* Fired when the mouseup event is captured on the cell.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event beforecellkeydown
* Fired before the cell key down is processed. Return false to cancel the default action.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event cellkeydown
* Fired when the keydown event is captured on the cell.
* @param {Ext.view.Table} this
* @param {HTMLElement} td The TD element for the cell.
* @param {Number} cellIndex
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event rowclick
* Fired when table cell is clicked.
* @param {Ext.view.Table} this
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event rowdblclick
* Fired when table cell is double clicked.
* @param {Ext.view.Table} this
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event rowcontextmenu
* Fired when table cell is right clicked.
* @param {Ext.view.Table} this
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event rowmousedown
* Fired when the mousedown event is captured on the cell.
* @param {Ext.view.Table} this
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event rowmouseup
* Fired when the mouseup event is captured on the cell.
* @param {Ext.view.Table} this
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
/**
* @event rowkeydown
* Fired when the keydown event is captured on the cell.
* @param {Ext.view.Table} this
* @param {Ext.data.Model} record
* @param {HTMLElement} tr The TR element for the cell.
* @param {Number} rowIndex
* @param {Ext.event.Event} e
* @param {Ext.grid.CellContext} e.position A CellContext object which defines the target cell.
*/
constructor: function(config) {
// Adjust our base class if we are inside a TreePanel
if (config.grid.isTree) {
config.baseCls = Ext.baseCSSPrefix + 'tree-view';
}
this.callParent([config]);
},
/**
* @private
* Returns `true` if this view has been configured with variableRowHeight (or this has been set by a plugin/feature)
* which might insert arbitrary markup into a grid item. Or if at least one visible column has been configured
* with variableRowHeight. Or if the store is grouped.
*/
hasVariableRowHeight: function(fromLockingPartner) {
var me = this;
return me.variableRowHeight || me.store.isGrouped() || me.getVisibleColumnManager().hasVariableRowHeight() ||
// If not already called from a locking partner, and there is a locking partner,
// and the partner has variableRowHeight, then WE have variableRowHeight too.
(!fromLockingPartner && me.lockingPartner && me.lockingPartner.hasVariableRowHeight(true));
},
initComponent: function() {
var me = this;
if (me.columnLines) {
me.addCls(me.grid.colLinesCls);
}
if (me.rowLines) {
me.addCls(me.grid.rowLinesCls);
}
/**
* @private
* @property {Ext.dom.Fly} body
* A flyweight Ext.Element which encapsulates a reference to the view's main row containing element.
* *Note that the `dom` reference will not be present until the first data refresh*
*/
me.body = new Ext.dom.Fly();
me.body.id = me.id + 'gridBody';
// If trackOver has been turned off, null out the overCls because documented behaviour
// in AbstractView is to turn trackOver on if overItemCls is set.
if (!me.trackOver) {
me.overItemCls = null;
}
me.headerCt.view = me;
// Features need a reference to the grid.
// Grid needs an immediate reference to its view so that the view can reliably be got from the grid during initialization
me.grid.view = me;
me.initFeatures(me.grid);
me.itemSelector = me.getItemSelector();
me.all = new Ext.view.NodeCache(me);
me.callParent();
},
// Private
// Create a config object for this view's selection model based upon the passed grid's configurations
applySelectionModel: function(selModel, oldSelModel) {
var me = this,
grid = me.ownerGrid,
defaultType = selModel.type;
// If this is the initial configuration, pull overriding configs in from the owning TablePanel.
if (!oldSelModel) {
// Favour a passed instance
if (!(selModel && selModel.isSelectionModel)) {
selModel = grid.selModel || selModel;
}
}
if (selModel) {
if (selModel.isSelectionModel) {
selModel.allowDeselect = grid.allowDeselect || selModel.selectionMode !== 'SINGLE';
selModel.locked = grid.disableSelection;
} else {
if (typeof selModel === 'string') {
selModel = {
type: selModel
};
}
// Copy obsolete selType property to type property now that selection models are Factoryable
// TODO: Remove selType config after deprecation period
else {
selModel.type = grid.selType || selModel.selType || selModel.type || defaultType;
}
if (!selModel.mode) {
if (grid.simpleSelect) {
selModel.mode = 'SIMPLE';
} else if (grid.multiSelect) {
selModel.mode = 'MULTI';
}
}
selModel = Ext.Factory.selection(Ext.apply({
allowDeselect: grid.allowDeselect,
locked: grid.disableSelection
}, selModel));
}
}
return selModel;
},
updateSelectionModel: function(selModel, oldSelModel) {
var me = this;
if (oldSelModel) {
oldSelModel.un({
scope: me,
lastselectedchanged: me.updateBindSelection,
selectionchange: me.updateBindSelection
});
Ext.destroy(me.selModelRelayer);
}
me.selModelRelayer = me.relayEvents(selModel, [
'selectionchange', 'beforeselect', 'beforedeselect', 'select', 'deselect', 'focuschange'
]);
selModel.on({
scope: me,
lastselectedchanged: me.updateBindSelection,
selectionchange: me.updateBindSelection
});
me.selModel = selModel;
},
getVisibleColumnManager: function() {
return this.ownerCt.getVisibleColumnManager();
},
getColumnManager: function() {
return this.ownerCt.getColumnManager();
},
getTopLevelVisibleColumnManager: function() {
// ownerGrid refers to the topmost responsible Ext.panel.Grid.
// This could be this view's ownerCt, or if part of a locking arrangement, the locking grid
return this.ownerGrid.getVisibleColumnManager();
},
/**
* @private
* Move a grid column from one position to another
* @param {Number} fromIdx The index from which to move columns
* @param {Number} toIdx The index at which to insert columns.
* @param {Number} [colsToMove=1] The number of columns to move beginning at the `fromIdx`
*/
moveColumn: function(fromIdx, toIdx, colsToMove) {
var me = this,
multiMove = colsToMove > 1,
range = multiMove && document.createRange ? document.createRange() : null,
fragment = multiMove && !range ? document.createDocumentFragment() : null,
destinationCellIdx = toIdx,
colCount = me.getGridColumns().length,
lastIndex = colCount - 1,
doFirstLastClasses = (me.firstCls || me.lastCls) && (toIdx === 0 || toIdx === colCount || fromIdx === 0 || fromIdx === lastIndex),
i,
j,
rows, len, tr, cells,
colGroups;
// Dragging between locked and unlocked side first refreshes the view, and calls onHeaderMoved with
// fromIndex and toIndex the same.
if (me.rendered && toIdx !== fromIdx) {
// Grab all rows which have column cells in.
// That is data rows.
rows = me.el.query(me.rowSelector);
if (toIdx > fromIdx && fragment) {
destinationCellIdx -= 1;
}
for (i = 0, len = rows.length; i < len; i++) {
tr = rows[i];
cells = tr.childNodes;
// Keep first cell class and last cell class correct *only if needed*
if (doFirstLastClasses) {
if (cells.length === 1) {
Ext.fly(cells[0]).addCls(me.firstCls);
Ext.fly(cells[0]).addCls(me.lastCls);
continue;
}
if (fromIdx === 0) {
Ext.fly(cells[0]).removeCls(me.firstCls);
Ext.fly(cells[1]).addCls(me.firstCls);
} else if (fromIdx === lastIndex) {
Ext.fly(cells[lastIndex]).removeCls(me.lastCls);
Ext.fly(cells[lastIndex - 1]).addCls(me.lastCls);
}
if (toIdx === 0) {
Ext.fly(cells[0]).removeCls(me.firstCls);
Ext.fly(cells[fromIdx]).addCls(me.firstCls);
} else if (toIdx === colCount) {
Ext.fly(cells[lastIndex]).removeCls(me.lastCls);
Ext.fly(cells[fromIdx]).addCls(me.lastCls);
}
}
// Move multi using the best technique.
// Extract a range straight into a fragment if possible.
if (multiMove) {
if (range) {
range.setStartBefore(cells[fromIdx]);
range.setEndAfter(cells[fromIdx + colsToMove - 1]);
fragment = range.extractContents();
}
else {
for (j = 0; j < colsToMove; j++) {
fragment.appendChild(cells[fromIdx]);
}
}
tr.insertBefore(fragment, cells[destinationCellIdx] || null);
} else {
tr.insertBefore(cells[fromIdx], cells[destinationCellIdx] || null);
}
}
// Shuffle the <col> elements in all <colgroup>s
colGroups = me.el.query('colgroup');
for (i = 0, len = colGroups.length; i < len; i++) {
// Extract the colgroup
tr = colGroups[i];
// Move multi using the best technique.
// Extract a range straight into a fragment if possible.
if (multiMove) {
if (range) {
range.setStartBefore(tr.childNodes[fromIdx]);
range.setEndAfter(tr.childNodes[fromIdx + colsToMove - 1]);
fragment = range.extractContents();
}
else {
for (j = 0; j < colsToMove; j++) {
fragment.appendChild(tr.childNodes[fromIdx]);
}
}
tr.insertBefore(fragment, tr.childNodes[destinationCellIdx] || null);
} else {
tr.insertBefore(tr.childNodes[fromIdx], tr.childNodes[destinationCellIdx] || null);
}
}
}
},
// scroll the view to the top
scrollToTop: Ext.emptyFn,
/**
* Add a listener to the main view element. It will be destroyed with the view.
* @private
*/
addElListener: function(eventName, fn, scope){
this.mon(this, eventName, fn, scope, {
element: 'el'
});
},
/**
* Get the leaf columns used for rendering the grid rows.
* @private
*/
getGridColumns: function() {
return this.ownerCt.getVisibleColumnManager().getColumns();
},
/**
* Get a leaf level header by index regardless of what the nesting
* structure is.
* @private
* @param {Number} index The index
*/
getHeaderAtIndex: function(index) {
return this.ownerCt.getVisibleColumnManager().getHeaderAtIndex(index);
},
/**
* Get the cell (td) for a particular record and column.
* @param {Ext.data.Model} record
* @param {Ext.grid.column.Column} column
* @private
*/
getCell: function(record, column) {
var row = this.getRow(record);
return Ext.fly(row).down(column.getCellSelector());
},
/**
* Get a reference to a feature
* @param {String} id The id of the feature
* @return {Ext.grid.feature.Feature} The feature. Undefined if not found
*/
getFeature: function(id) {
var features = this.featuresMC;
if (features) {
return features.get(id);
}
},
// @private
// Finds a features by ftype in the features array
findFeature: function(ftype) {
if (this.features) {
return Ext.Array.findBy(this.features, function(feature) {
if (feature.ftype === ftype) {
return true;
}
});
}
},
/**
* Initializes each feature and bind it to this view.
* @private
*/
initFeatures: function(grid) {
var me = this,
i,
features,
feature,
len;
// Row container element emitted by tpl
me.tpl = Ext.XTemplate.getTpl(this, 'tpl');
// The rowTpl emits a <div>
me.rowTpl = Ext.XTemplate.getTpl(this, 'rowTpl');
me.addRowTpl(Ext.XTemplate.getTpl(this, 'outerRowTpl'));
// Each cell is emitted by the cellTpl
me.cellTpl = Ext.XTemplate.getTpl(this, 'cellTpl');
me.featuresMC = new Ext.util.MixedCollection();
features = me.features = me.constructFeatures();
len = features ? features.length : 0;
for (i = 0; i < len; i++) {
feature = features[i];
// inject a reference to view and grid - Features need both
feature.view = me;
feature.grid = grid;
me.featuresMC.add(feature);
feature.init(grid);
}
},
renderTHead: function(values, out, parent) {
var headers = values.view.headerFns,
len, i;
if (headers) {
for (i = 0, len = headers.length; i < len; ++i) {
headers[i].call(this, values, out, parent);
}
}
},
// Currently, we don't have ordering support for header/footer functions,
// they will be pushed on at construction time. If the need does arise,
// we can add this functionality in the future, but for now it's not
// really necessary since currently only the summary feature uses this.
addHeaderFn: function(fn) {
var headers = this.headerFns;
if (!headers) {
headers = this.headerFns = [];
}
headers.push(fn);
},
renderTFoot: function(values, out, parent){
var footers = values.view.footerFns,
len, i;
if (footers) {
for (i = 0, len = footers.length; i < len; ++i) {
footers[i].call(this, values, out, parent);
}
}
},
addFooterFn: function(fn) {
var footers = this.footerFns;
if (!footers) {
footers = this.footerFns = [];
}
footers.push(fn);
},
addTpl: function(newTpl) {
return this.insertTpl('tpl', newTpl);
},
addRowTpl: function(newTpl) {
return this.insertTpl('rowTpl', newTpl);
},
addCellTpl: function(newTpl) {
return this.insertTpl('cellTpl', newTpl);
},
insertTpl: function(which, newTpl) {
var me = this,
tpl,
prevTpl;
// Clone an instantiated XTemplate
if (newTpl.isTemplate) {
newTpl = Ext.Object.chain(newTpl);
}
// If we have been passed an object of the form
// {
// before: fn
// after: fn
// }
// Create a template from it using the object as the member configuration
else {
newTpl = new Ext.XTemplate('{%this.nextTpl.applyOut(values, out, parent);%}', newTpl);
}
// Stop at the first TPL who's priority is less than the passed rowTpl
for (tpl = me[which]; newTpl.priority < tpl.priority; tpl = tpl.nextTpl) {
prevTpl = tpl;
}
// If we had skipped over some, link the previous one to the passed rowTpl
if (prevTpl) {
prevTpl.nextTpl = newTpl;
}
// First one
else {
me[which] = newTpl;
}
newTpl.nextTpl = tpl;
return newTpl;
},
tplApplyOut: function(values, out, parent) {
if (this.before) {
if (this.before(values, out, parent) === false) {
return;
}
}
this.nextTpl.applyOut(values, out, parent);
if (this.after) {
this.after(values, out, parent);
}
},
/**
* @private
* Converts the features array as configured, into an array of instantiated Feature objects.
*
* Must have no side effects other than Feature instantiation.
*
* MUST NOT update the this.features property, and MUST NOT update the instantiated Features.
*/
constructFeatures: function() {
var me = this,
features = me.features,
feature,
result,
i = 0, len;
if (features) {
result = [];
len = features.length;
for (; i < len; i++) {
feature = features[i];
if (!feature.isFeature) {
feature = Ext.create('feature.' + feature.ftype, feature);
}
result[i] = feature;
}
}
return result;
},
beforeRender: function() {
var me = this;
me.callParent();
if (!me.enableTextSelection) {
me.protoEl.unselectable();
}
},
onBindStore: function(store) {
var me = this,
bufferedRenderer = me.bufferedRenderer;
if (bufferedRenderer && bufferedRenderer.store !== store) {
bufferedRenderer.bindStore(store);
}
// Reset virtual scrolling.
if (me.all && me.all.getCount()) {
if (bufferedRenderer) {
bufferedRenderer.setBodyTop(0);
}
me.clearViewEl();
}
me.callParent(arguments);
},
getStoreListeners: function() {
var result = this.callParent();
result.beforepageremove = this.beforePageRemove;
return result;
},
beforePageRemove: function(pageMap, pageNumber) {
var rows = this.all,
pageSize = pageMap.getPageSize();
// If the rendered block needs the page, access it which moves it to the end of the LRU cache, and veto removal.
if (rows.startIndex >= (pageNumber - 1) * pageSize && rows.endIndex <= (pageNumber * pageSize - 1)) {
pageMap.get(pageNumber);
return false;
}
},
// Private template method implemented starting at the AbstractView class.
onViewScroll: function(scroller, x, y) {
// We ignore scrolling caused by focusing
if (!this.ignoreScroll) {
this.callParent([scroller, x, y]);
}
},
// private
// Create the DOM element which enapsulates the passed record.
// Used when updating existing rows, so drills down into resulting structure .
createRowElement: function(record, index, updateColumns) {
var me = this,
div = me.renderBuffer,
tplData = me.collectData([record], index);
tplData.columns = updateColumns;
me.tpl.overwrite(div, tplData);
// Return first element within node containing element
return Ext.fly(div).down(me.getNodeContainerSelector(), true).firstChild;
},
// private
// Override so that we can use a quicker way to access the row nodes.
// They are simply all child nodes of the nodeContainer element.
bufferRender: function(records, index) {
var me = this,
div = me.renderBuffer,
result,
range = document.createRange ? document.createRange() : null;
me.tpl.overwrite(div, me.collectData(records, index));
div = Ext.fly(div).down(me.getNodeContainerSelector(), true);
if (range) {
range.selectNodeContents(div);
result = range.extractContents();
} else {
result = document.createDocumentFragment();
while (div.firstChild) {
result.appendChild(div.firstChild);
}
}
return {
fragment: result,
children: Ext.Array.toArray(result.childNodes)
};
},
collectData: function(records, startIndex) {
var me = this;
me.rowValues.view = me;
me.tableValues.view = me;
me.tableValues.rows = records;
me.tableValues.columns = null;
me.tableValues.viewStartIndex = startIndex;
me.tableValues.touchScroll = me.touchScroll;
me.tableValues.tableStyle = 'width:' + me.headerCt.getTableWidth() + 'px';
return me.tableValues;
},
// Overridden implementation.
// Called by refresh to collect the view item nodes.
// Note that these may be wrapping rows which *contain* rows which map to records
collectNodes: function(targetEl) {
this.all.fill(this.getNodeContainer().childNodes, this.all.startIndex);
},
// Private. Called when the table changes height.
// For example, see examples/grid/group-summary-grid.html
// If we have flexed column headers, we need to update the header layout
// because it may have to accommodate (or cease to accommodate) a vertical scrollbar.
// Only do this on platforms which have a space-consuming scrollbar.
// Only do it when vertical scrolling is enabled.
refreshSize: function(forceLayout) {
var me = this,
bodySelector = me.getBodySelector();
// On every update of the layout system due to data update, capture the view's main element in our private flyweight.
// IF there *is* a main element. Some TplFactories emit naked rows.
if (bodySelector) {
// use "down" instead of "child" because the grid table element is not a direct
// child of the view element when a touch scroller is in use.
me.body.attach(me.el.down(bodySelector, true));
}
if (!me.hasLoadingHeight) {
// Suspend layouts in case the superclass requests a layout. We might too, so they
// must be coalesced.
Ext.suspendLayouts();
me.callParent(arguments);
// We only need to adjust for height changes in the data if we, or any visible columns have been configured with
// variableRowHeight: true
// OR, if we are being passed the forceUpdate flag which is passed when the view's item count changes.
if (forceLayout || (me.hasVariableRowHeight() && me.dataSource.getCount())) {
me.grid.updateLayout();
}
Ext.resumeLayouts(true);
}
},
clearViewEl: function(leaveNodeContainer) {
var me = this,
all = me.all,
store = me.getStore(),
i, item, nodeContainer, targetEl;
// The purpose of this is to allow boilerplate HTML nodes to remain in place inside a View
// while the transient, templated data can be discarded and recreated.
//
// In particular, this is used in infinite grid scrolling: A very tall "stretcher" element is
// inserted into the View's element to create a scrollbar of the correct proportion.
//
// Also we must ensure that the itemremove event is fired EVERY time an item is removed from the
// view. This is so that widgets rendered into a view by a WidgetColumn can be recycled.
for (i = all.startIndex; i <= all.endIndex; i++) {
item = all.item(i, true);
me.fireEvent('itemremove', store.getByInternalId(item.getAttribute('data-recordId')), i, item, me);
}
// AbstractView will clear the view correctly
// It also resets the scrollrange.
me.callParent();
nodeContainer = Ext.fly(me.getNodeContainer());
if (nodeContainer && !leaveNodeContainer) {
targetEl = me.getTargetEl();
if (targetEl.dom !== nodeContainer.dom) {
nodeContainer.destroy();
}
}
},
getMaskTarget: function() {
// Masking a TableView masks its IMMEDIATE parent GridPanel's body.
// Disabling/enabling a locking view relays the call to both child views.
return this.ownerCt.body;
},
statics: {
getBoundView: function(node) {
return Ext.getCmp(node.getAttribute('data-boundView'));
}
},
getRecord: function(node) {
var me = this,
recordIndex;
// If store.destroy has been called before some delayed event fires on a node, we must ignore the event.
if (me.store.isDestroyed) {
return;
}
if (node.isModel) {
return node;
}
node = me.getNode(node);
if (node) {
// The indices may be off because of collapsed groups (if we're grouping) or row wrapping, so just grab it by id.
if (!me.hasActiveFeature()) {
recordIndex = node.getAttribute('data-recordIndex');
if (recordIndex) {
recordIndex = parseInt(recordIndex, 10);
if (recordIndex > -1) {
// The index is the index in the original Store, not in a GroupStore
// The Grouping Feature increments the index to skip over unrendered records in collapsed groups
return me.store.data.getAt(recordIndex);
}
}
}
return me.dataSource.getByInternalId(node.getAttribute('data-recordId'));
}
},
indexOf: function(node) {
node = this.getNode(node);
if (!node && node !== 0) {
return -1;
}
return this.all.indexOf(node);
},
indexInStore: function(node) {
// We cannot use the stamped in data-recordindex because that is the index in the original configured store
// NOT the index in the dataSource that is being used - that may be a GroupStore.
return node ? this.dataSource.indexOf(this.getRecord(node)) : -1;
},
renderRows: function(rows, columns, viewStartIndex, out) {
var rowValues = this.rowValues,
rowCount = rows.length,
i;
rowValues.view = this;
rowValues.columns = columns;
for (i = 0; i < rowCount; i++, viewStartIndex++) {
rowValues.itemClasses.length = rowValues.rowClasses.length = 0;
this.renderRow(rows[i], viewStartIndex, out);
}
// Dereference objects since rowValues is a persistent on our prototype
rowValues.view = rowValues.columns = rowValues.record = null;
},
/* Alternative column sizer element renderer.
renderTHeadColumnSizer: function(values, out) {
var columns = this.getGridColumns(),
len = columns.length, i,
column, width;
out.push('<thead><tr class="' + Ext.baseCSSPrefix + 'grid-header-row">');
for (i = 0; i < len; i++) {
column = columns[i];
width = column.lastBox ? column.lastBox.width : Ext.grid.header.Container.prototype.defaultWidth;
out.push('<th class="', Ext.baseCSSPrefix, 'grid-cell-', columns[i].getItemId(), '" style="width:' + width + 'px"></th>');
}
out.push('</tr></thead>');
},
*/
renderColumnSizer: function(values, out) {
var columns = values.columns || this.getGridColumns(),
len = columns.length, i,
column, width;
out.push('<colgroup role="presentation">');
for (i = 0; i < len; i++) {
column = columns[i];
width = column.cellWidth ? column.cellWidth : Ext.grid.header.Container.prototype.defaultWidth;
out.push('<col role="presentation" class="', Ext.baseCSSPrefix, 'grid-cell-', columns[i].getItemId(), '" style="width:' + width + 'px">');
}
out.push('</colgroup>');
},
/**
* @private
* Renders the HTML markup string for a single row into the passed array as a sequence of strings, or
* returns the HTML markup for a single row.
*
* @param {Ext.data.Model} record The record to render.
* @param {String[]} [out] A string array onto which to append the resulting HTML string. If omitted,
* the resulting HTML string is returned.
* @return {String} **only when the out parameter is omitted** The resulting HTML string.
*/
renderRow: function(record, rowIdx, out) {
var me = this,
isMetadataRecord = rowIdx === -1,
selModel = me.selectionModel,
rowValues = me.rowValues,
itemClasses = rowValues.itemClasses,
rowClasses = rowValues.rowClasses,
itemCls = me.itemCls,
cls,
rowTpl = me.rowTpl;
// Define the rowAttr object now. We don't want to do it in the treeview treeRowTpl because anything
// this is processed in a deferred callback (such as deferring initial view refresh in gridview) could
// poke rowAttr that are then shared in tableview.rowTpl. See EXTJSIV-9341.
//
// For example, the following shows the shared ref between a treeview's rowTpl nextTpl and the superclass
// tableview.rowTpl:
//
// tree.view.rowTpl.nextTpl === grid.view.rowTpl
//
rowValues.rowAttr = {};
// Set up mandatory properties on rowValues
rowValues.record = record;
rowValues.recordId = record.internalId;
// recordIndex is index in true store (NOT the data source - possibly a GroupStore)
rowValues.recordIndex = me.store.indexOf(record);
// rowIndex is the row number in the view.
rowValues.rowIndex = rowIdx;
rowValues.rowId = me.getRowId(record);
rowValues.itemCls = rowValues.rowCls = '';
if (!rowValues.columns) {
rowValues.columns = me.ownerCt.getVisibleColumnManager().getColumns();
}
itemClasses.length = rowClasses.length = 0;
// If it's a metadata record such as a summary record.
// So do not decorate it with the regular CSS.
// The Feature which renders it must know how to decorate it.
if (!isMetadataRecord) {
itemClasses[0] = itemCls;
if (!me.ownerCt.disableSelection && selModel.isRowSelected) {
// Selection class goes on the outermost row, so it goes into itemClasses
if (selModel.isRowSelected(record)) {
itemClasses.push(me.selectedItemCls);
}
}
if (me.stripeRows && rowIdx % 2 !== 0) {
itemClasses.push(me.altRowCls);
}
if (me.getRowClass) {
cls = me.getRowClass(record, rowIdx, null, me.dataSource);
if (cls) {
rowClasses.push(cls);
}
}
}
if (out) {
rowTpl.applyOut(rowValues, out, me.tableValues);
} else {
return rowTpl.apply(rowValues, me.tableValues);
}
},
/**
* @private
* Emits the HTML representing a single grid cell into the passed output stream (which is an array of strings).
*
* @param {Ext.grid.column.Column} column The column definition for which to render a cell.
* @param {Number} recordIndex The row index (zero based within the {@link #store}) for which to render the cell.
* @param {Number} rowIndex The row index (zero based within this view for which to render the cell.
* @param {Number} columnIndex The column index (zero based) for which to render the cell.
* @param {String[]} out The output stream into which the HTML strings are appended.
*/
renderCell: function (column, record, recordIndex, rowIndex, columnIndex, out) {
var me = this,
fullIndex,
selModel = me.selectionModel,
cellValues = me.cellValues,
classes = cellValues.classes,
fieldValue = record.data[column.dataIndex],
cellTpl = me.cellTpl,
value, clsInsertPoint,
lastFocused = me.navigationModel.getPosition();
cellValues.record = record;
cellValues.column = column;
cellValues.recordIndex = recordIndex;
cellValues.rowIndex = rowIndex;
cellValues.columnIndex = columnIndex;
cellValues.cellIndex = columnIndex;
cellValues.align = column.align;
cellValues.innerCls = column.innerCls;
cellValues.tdCls = cellValues.tdStyle = cellValues.tdAttr = cellValues.style = "";
cellValues.unselectableAttr = me.enableTextSelection ? '' : 'unselectable="on"';
// Begin setup of classes to add to cell
classes[1] = column.getCellId();
// On IE8, array[len] = 'foo' is twice as fast as array.push('foo')
// So keep an insertion point and use assignment to help IE!
clsInsertPoint = 2;
if (column.renderer && column.renderer.call) {
fullIndex = me.ownerCt.columnManager.getHeaderIndex(column);
value = column.renderer.call(column.usingDefaultRenderer ? column : column.scope || me.ownerCt, fieldValue, cellValues, record, recordIndex, fullIndex, me.dataSource, me);
if (cellValues.css) {
// This warning attribute is used by the compat layer
// TODO: remove when compat layer becomes deprecated
record.cssWarning = true;
cellValues.tdCls += ' ' + cellValues.css;
cellValues.css = null;
}
// Add any tdCls which was added to the cellValues by the renderer.
if (cellValues.tdCls) {
classes[clsInsertPoint++] = cellValues.tdCls;
}
} else {
value = fieldValue;
}
cellValues.value = (value == null || value === '') ? column.emptyCellText : value;
if (column.tdCls) {
classes[clsInsertPoint++] = column.tdCls;
}
if (me.markDirty && record.dirty && record.isModified(column.dataIndex)) {
classes[clsInsertPoint++] = me.dirtyCls;
}
if (column.isFirstVisible) {
classes[clsInsertPoint++] = me.firstCls;
}
if (column.isLastVisible) {
classes[clsInsertPoint++] = me.lastCls;
}
if (!me.enableTextSelection) {
classes[clsInsertPoint++] = me.unselectableCls;
}
if (selModel && (selModel.isCellModel || selModel.isSpreadsheetModel) && selModel.isCellSelected(me, recordIndex, column)) {
classes[clsInsertPoint++] = me.selectedCellCls;
}
if (lastFocused && lastFocused.record.id === record.id && lastFocused.column === column) {
classes[clsInsertPoint++] = me.focusedItemCls;
}
// Chop back array to only what we've set
classes.length = clsInsertPoint;
cellValues.tdCls = classes.join(' ');
cellTpl.applyOut(cellValues, out);
// Dereference objects since cellValues is a persistent var in the XTemplate's scope chain
cellValues.column = null;
},
/**
* Returns the table row given the passed Record, or index or node.
* @param {HTMLElement/String/Number/Ext.data.Model} nodeInfo The node or record, or row index.
* to return the top level row.
* @return {HTMLElement} The node or null if it wasn't found
*/
getRow: function(nodeInfo) {
var fly;
if ((!nodeInfo && nodeInfo !== 0) || !this.rendered) {
return null;
}
// An event
if (nodeInfo.target) {
nodeInfo = nodeInfo.target;
}
// An id
if (Ext.isString(nodeInfo)) {
return Ext.fly(nodeInfo).down(this.rowSelector,true);
}
// Row index
if (Ext.isNumber(nodeInfo)) {
fly = this.all.item(nodeInfo);
return fly && fly.down(this.rowSelector, true);
}
// Record
if (nodeInfo.isModel) {
return this.getRowByRecord(nodeInfo);
}
fly = Ext.fly(nodeInfo);
// Passed an item, go down and get the row
if (fly.is(this.itemSelector)) {
return this.getRowFromItem(fly);
}
// Passed a child element of a row
return fly.findParent(this.rowSelector, this.getTargetEl()); // already an HTMLElement
},
getRowId: function(record){
return this.id + '-record-' + record.internalId;
},
constructRowId: function(internalId){
return this.id + '-record-' + internalId;
},
getNodeById: function(id){
id = this.constructRowId(id);
return this.retrieveNode(id, false);
},
getRowById: function(id){
id = this.constructRowId(id);
return this.retrieveNode(id, true);
},
getNodeByRecord: function(record) {
return this.retrieveNode(this.getRowId(record), false);
},
getRowByRecord: function(record) {
return this.retrieveNode(this.getRowId(record), true);
},
getRowFromItem: function(item) {
var rows = Ext.getDom(item).tBodies[0].childNodes,
len = rows.length,
i;
for (i = 0; i < len; i++) {
if (Ext.fly(rows[i]).is(this.rowSelector)) {
return rows[i];
}
}
},
retrieveNode: function(id, dataRow){
var result = this.el.getById(id, true);
if (dataRow && result) {
return Ext.fly(result).down(this.rowSelector, true);
}
return result;
},
// Links back from grid rows are installed by the XTemplate as data attributes
updateIndexes: Ext.emptyFn,
// Outer table
bodySelector: 'div.' + Ext.baseCSSPrefix + 'grid-item-container',
// Element which contains rows
nodeContainerSelector: 'div.' + Ext.baseCSSPrefix + 'grid-item-container',
// view item. This wraps a data row
itemSelector: 'table.' + Ext.baseCSSPrefix + 'grid-item',
// Grid row which contains cells as opposed to wrapping item.
rowSelector: 'tr.' + Ext.baseCSSPrefix + 'grid-row',
// cell
cellSelector: 'td.' + Ext.baseCSSPrefix + 'grid-cell',
// Select column sizers and cells.
// This may target `<COL>` elements as well as `<TD>` elements
// `<COLGROUP>` element is inserted if the first row does not have the regular cell patten (eg is a colspanning group header row)
sizerSelector: '.' + Ext.baseCSSPrefix + 'grid-cell',
innerSelector: 'div.' + Ext.baseCSSPrefix + 'grid-cell-inner',
/**
* Returns a CSS selector which selects the outermost element(s) in this view.
*/
getBodySelector: function() {
return this.bodySelector;
},
/**
* Returns a CSS selector which selects the element(s) which define the width of a column.
*
* This is used by the {@link Ext.view.TableLayout} when resizing columns.
*
*/
getColumnSizerSelector: function(header) {
var selector = this.sizerSelector + '-' + header.getItemId();
return 'td' + selector + ',col' + selector;
},
/**
* Returns a CSS selector which selects items of the view rendered by the outerRowTpl
*/
getItemSelector: function() {
return this.itemSelector;
},
/**
* Returns a CSS selector which selects a particular column if the desired header is passed,
* or a general cell selector is no parameter is passed.
*
* @param {Ext.grid.column.Column} [header] The column for which to return the selector. If
* omitted, the general cell selector which matches **ant cell** will be returned.
*
*/
getCellSelector: function(header) {
return header ? header.getCellSelector() : this.cellSelector;
},
/*
* Returns a CSS selector which selects the content carrying element within cells.
*/
getCellInnerSelector: function(header) {
return this.getCellSelector(header) + ' ' + this.innerSelector;
},
/**
* Adds a CSS Class to a specific row.
* @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model
* representing this row
* @param {String} cls
*/
addRowCls: function(rowInfo, cls) {
var row = this.getRow(rowInfo);
if (row) {
Ext.fly(row).addCls(cls);
}
},
/**
* Removes a CSS Class from a specific row.
* @param {HTMLElement/String/Number/Ext.data.Model} rowInfo An HTMLElement, index or instance of a model
* representing this row
* @param {String} cls
*/
removeRowCls: function(rowInfo, cls) {
var row = this.getRow(rowInfo);
if (row) {
Ext.fly(row).removeCls(cls);
}
},
// GridSelectionModel invokes onRowSelect as selection changes
onRowSelect: function(rowIdx) {
var me = this;
me.addItemCls(rowIdx, me.selectedItemCls);
//<feature legacyBrowser>
if (Ext.isIE8) {
me.repaintBorder(rowIdx + 1);
}
//</feature>
},
// GridSelectionModel invokes onRowDeselect as selection changes
onRowDeselect: function(rowIdx) {
var me = this;
me.removeItemCls(rowIdx, me.selectedItemCls);
//<feature legacyBrowser>
if (Ext.isIE8) {
me.repaintBorder(rowIdx + 1);
}
//</feature>
},
onCellSelect: function(position) {
var cell = this.getCellByPosition(position);
if (cell) {
cell.addCls(this.selectedCellCls);
}
},
onCellDeselect: function(position) {
var cell = this.getCellByPosition(position, true);
if (cell) {
Ext.fly(cell).removeCls(this.selectedCellCls);
}
},
// Old API. Used by tests now to test coercion of navigation from hidden column to closest visible.
// Position.column includes all columns including hidden ones.
getCellInclusive: function(position, returnDom) {
if (position) {
var row = this.getRow(position.row),
header = this.ownerCt.getColumnManager().getHeaderAtIndex(position.column);
if (header && row) {
return Ext.fly(row).down(this.getCellSelector(header), returnDom);
}
}
return false;
},
getCellByPosition: function(position, returnDom) {
if (position) {
var view = position.view || this,
row = view.getRow(position.record || position.row),
header = position.column.isColumn ? position.column : view.getVisibleColumnManager().getHeaderAtIndex(position.column);
if (header && row) {
return Ext.fly(row).down(view.getCellSelector(header), returnDom);
}
}
return false;
},
onFocusEnter: function(e) {
var me = this,
targetView,
navigationModel = me.getNavigationModel(),
lastFocused,
focusPosition,
br = me.bufferedRenderer,
firstRecord,
focusTarget;
// The underlying DOM event
e = e.event;
// We can only focus if there are rows in the row cache to focus *and* records
// in the store to back them. Buffered Stores can produce a state where
// the view is not cleared on the leading end of a reload operation, but the
// store can be empty.
if (!me.cellFocused && me.all.getCount() && me.dataSource.getCount()) {
focusTarget = e.getTarget();
// If what is being focused an interior element, but is not a cell, allow it to proceed.
// The position silently restores to what it was when we were focused last.
if (focusTarget && me.el.contains(focusTarget) && focusTarget !== me.el.dom && !Ext.fly(focusTarget).is(me.getCellSelector())) {
if (navigationModel.lastFocused) {
navigationModel.position = navigationModel.lastFocused;
}
me.cellFocused = true;
} else {
lastFocused = focusPosition = me.getLastFocused();
// Default to the first cell if the NavigationModel has never focused anything
if (!focusPosition) {
targetView = me.isNormalView ? (me.lockingPartner.isVisible() ? me.lockingPartner : me.normalView) : me;
firstRecord = me.dataSource.getAt(br ? br.getFirstVisibleRowIndex() : 0);
// A non-row producing record like a collapsed placeholder.
// We cannot focus these yet.
if (firstRecord && !firstRecord.isNonData) {
focusPosition = new Ext.grid.CellContext(targetView).setPosition({
row: firstRecord,
column: 0
});
}
}
// Not a descendant which we allow to carry focus. Blur it.
if (!focusPosition) {
e.stopEvent();
e.getTarget().blur();
return;
}
navigationModel.setPosition(focusPosition, null, e, null, true);
// We now contain focus is that was successful
me.cellFocused = !!navigationModel.getPosition();
}
}
if (me.cellFocused) {
me.el.dom.setAttribute('tabindex', '-1');
}
},
onFocusLeave: function(e) {
var me = this;
// Ignore this event if we do not actually contain focus.
// CellEditors are rendered into the view's encapculating element,
// So focusleave will fire when they are programatically blurred.
// We will not have focus at that point.
if (me.cellFocused) {
// Blur the focused cell unless we are navigating into a locking partner,
// in which case, the focus of that will setPosition to the target
// without an intervening position to null.
if (e.toComponent !== me.lockingPartner) {
me.getNavigationModel().setPosition(null, null, e.event, null, true);
}
me.cellFocused = false;
me.focusEl = me.el;
me.focusEl.dom.setAttribute('tabindex', 0);
}
},
// GridSelectionModel invokes onRowFocus to 'highlight'
// the last row focused
onRowFocus: function(rowIdx, highlight, supressFocus) {
var me = this;
if (highlight) {
me.addItemCls(rowIdx, me.focusedItemCls);
if (!supressFocus) {
me.focusRow(rowIdx);
}
//this.el.dom.setAttribute('aria-activedescendant', row.id);
} else {
me.removeItemCls(rowIdx, me.focusedItemCls);
}
//<feature legacyBrowser>
if (Ext.isIE8) {
me.repaintBorder(rowIdx + 1);
}
//</feature>
},
/**
* Focuses a particular row and brings it into view. Will fire the rowfocus event.
* @param {HTMLElement/String/Number/Ext.data.Model} row An HTMLElement template node, index of a template node, the id of a template node or the
* @param {Boolean/Number} [delay] Delay the focus this number of milliseconds (true for 10 milliseconds).
* record associated with the node.
*/
focusRow: function(row, delay) {
var me = this,
focusTask = me.getFocusTask();
if (delay) {
focusTask.delay(Ext.isNumber(delay) ? delay : 10, me.focusRow, me, [row, false]);
return;
}
// An immediate focus call must cancel any outstanding delayed focus calls.
focusTask.cancel();
// Do not attempt to focus if hidden or within collapsed Panel.
if (me.isVisible(true)) {
me.getNavigationModel().setPosition(me.getRecord(row));
}
},
// Override the version in Ext.view.View because the focusable elements are the grid cells.
/**
* @override
* Focuses a particular row and brings it into view. Will fire the rowfocus event.
* @param {HTMLElement/String/Number/Ext.data.Model} row An HTMLElement template node, index of a template node, the id of a template node or the
* @param {Boolean/Number} [delay] Delay the focus this number of milliseconds (true for 10 milliseconds).
* record associated with the node.
*/
focusNode: function(row, delay) {
this.focusRow(row, delay);
},
scrollRowIntoView: function(row, animate) {
row = this.getRow(row);
if (row) {
this.scrollElIntoView(row, false, animate);
}
},
/**
* Focuses a particular cell and brings it into view. Will fire the rowfocus event.
* @param {Ext.grid.CellContext} pos The cell to select
* @param {Boolean/Number} [delay] Delay the focus this number of milliseconds (true for 10 milliseconds).
*/
focusCell: function(position, delay) {
var me = this,
cell,
focusTask = me.getFocusTask();
if (delay) {
focusTask.delay(Ext.isNumber(delay) ? delay : 10, me.focusCell, me, [position, false]);
return;
}
// An immediate focus call must cancel any outstanding delayed focus calls.
focusTask.cancel();
// Do not attempt to focus if hidden or within collapsed Panel
// Maintainer: Note that to avoid an unnecessary call to me.getCellByPosition if not visible, or another, nested if test,
// the assignment of the cell var is embedded inside the condition expression.
if (me.isVisible(true) && (cell = me.getCellByPosition(position))) {
me.getNavigationModel().setPosition(position);
}
},
getLastFocused: function() {
var me = this,
lastFocused = me.lastFocused;
if (lastFocused && lastFocused.record && lastFocused.column) {
// If the last focused record or column has gone away, or the record is no longer in the visible rendered block, we have no lastFocused
if (me.dataSource.indexOf(lastFocused.record) !== -1 && me.getVisibleColumnManager().indexOf(lastFocused.column) !== -1 && me.getNode(lastFocused.record)) {
return lastFocused;
}
}
},
scrollCellIntoView: function(cell, animate) {
if (cell.isCellContext) {
cell = this.getCellByPosition(cell);
}
if (cell) {
this.scrollElIntoView(cell, null, animate);
}
},
scrollElIntoView: function(el, hscroll, animate) {
var scroller = this.getScrollable();
if (scroller) {
scroller.scrollIntoView(el, hscroll, animate);
}
},
syncRowHeightBegin: function () {
var me = this,
itemEls = me.all,
ln = itemEls.count,
synchronizer = [],
RowSynchronizer = Ext.grid.locking.RowSynchronizer,
i, j, rowSync;
for (i = 0, j = itemEls.startIndex; i < ln; i++, j++) {
synchronizer[i] = rowSync = new RowSynchronizer(me, itemEls.elements[j]);
rowSync.reset();
}
return synchronizer;
},
syncRowHeightClear: function (synchronizer) {
var me = this,
itemEls = me.all,
ln = itemEls.count,
i;
for (i = 0; i < ln; i++) {
synchronizer[i].reset();
}
},
syncRowHeightMeasure: function (synchronizer) {
var ln = synchronizer.length,
i;
for (i = 0; i < ln; i++) {
synchronizer[i].measure();
}
},
syncRowHeightFinish: function (synchronizer, otherSynchronizer) {
var ln = synchronizer.length,
bufferedRenderer = this.bufferedRenderer,
i;
for (i = 0; i < ln; i++) {
synchronizer[i].finish(otherSynchronizer[i]);
}
// Ensure that both BufferedRenderers have the same idea about scroll range and row height
if (bufferedRenderer) {
bufferedRenderer.syncRowHeightsFinish();
}
},
// private
handleUpdate: function(store, record, operation, changedFieldNames) {
operation = operation || Ext.data.Model.EDIT;
var me = this,
rowTpl = me.rowTpl,
markDirty = me.markDirty,
dirtyCls = me.dirtyCls,
clearDirty = operation !== Ext.data.Model.EDIT,
columnsToUpdate = [],
hasVariableRowHeight = me.variableRowHeight,
updateTypeFlags = 0,
ownerCt = me.ownerCt,
cellFly = me.cellFly || (me.self.prototype.cellFly = new Ext.dom.Fly()),
oldItem, oldItemDom, oldDataRow,
newItemDom,
newAttrs, attLen, attName, attrIndex,
overItemCls,
columns,
column,
len, i,
cellUpdateFlag,
cell,
fieldName,
value,
defaultRenderer,
scope,
elData,
emptyValue;
if (me.viewReady) {
// Table row being updated
oldItemDom = me.getNodeByRecord(record);
// Row might not be rendered due to buffered rendering or being part of a collapsed group...
if (oldItemDom) {
overItemCls = me.overItemCls;
columns = me.ownerCt.getVisibleColumnManager().getColumns();
// Collect an array of the columns which must be updated.
// If the field at this column index was changed, or column has a custom renderer
// (which means value could rely on any other changed field) we include the column.
for (i = 0, len = columns.length; i < len; i++) {
column = columns[i];
// We are not going to update the cell, but we still need to mark it as dirty.
if (column.preventUpdate) {
cell = Ext.fly(oldItemDom).down(column.getCellSelector(), true);
// Mark the field's dirty status if we are configured to do so (defaults to true)
if (!clearDirty && markDirty) {
cellFly.attach(cell);
if (record.isModified(column.dataIndex)) {
cellFly.addCls(dirtyCls);
} else {
cellFly.removeCls(dirtyCls);
}
}
} else {
// 0 = Column doesn't need update.
// 1 = Column needs update, and renderer has > 1 argument; We need to render a whole new HTML item.
// 2 = Column needs update, but renderer has 1 argument or column uses an updater.
cellUpdateFlag = me.shouldUpdateCell(record, column, changedFieldNames);
if (cellUpdateFlag) {
// Track if any of the updating columns yields a flag with the 1 bit set.
// This means that there is a custom renderer involved and a new TableView item
// will need rendering.
updateTypeFlags = updateTypeFlags | cellUpdateFlag; // jshint ignore:line
columnsToUpdate[columnsToUpdate.length] = column;
hasVariableRowHeight = hasVariableRowHeight || column.variableRowHeight;
}
}
}
// If there's no data row (some other rowTpl has been used; eg group header)
// or we have a getRowClass
// or one or more columns has a custom renderer
// or there's more than one <TR>, we must use the full render pathway to create a whole new TableView item
if (me.getRowClass || !me.getRowFromItem(oldItemDom) ||
(updateTypeFlags & 1) || // jshint ignore:line
(oldItemDom.tBodies[0].childNodes.length > 1)) {
oldItem = Ext.fly(oldItemDom, '_internal');
elData = oldItemDom._extData;
newItemDom = me.createRowElement(record, me.dataSource.indexOf(record), columnsToUpdate);
if (oldItem.hasCls(overItemCls)) {
Ext.fly(newItemDom).addCls(overItemCls);
}
// Copy new row attributes across. Use IE-specific method if possible.
// In IE10, there is a problem where the className will not get updated
// in the view, even though the className on the dom element is correct.
// See EXTJSIV-9462
if (Ext.isIE9m && oldItemDom.mergeAttributes) {
oldItemDom.mergeAttributes(newItemDom, true);
} else {
newAttrs = newItemDom.attributes;
attLen = newAttrs.length;
for (attrIndex = 0; attrIndex < attLen; attrIndex++) {
attName = newAttrs[attrIndex].name;
if (attName !== 'id') {
oldItemDom.setAttribute(attName, newAttrs[attrIndex].value);
}
}
}
// The element's data is no longer synchronized. We just overwrite it in the DOM
if (elData) {
elData.isSynchronized = false;
}
// If we have columns which may *need* updating (think locked side of lockable grid with all columns unlocked)
// and the changed record is within our view, then update the view.
if (columns.length && (oldDataRow = me.getRow(oldItemDom))) {
me.updateColumns(oldDataRow, Ext.fly(newItemDom).down(me.rowSelector, true), columnsToUpdate);
}
// Loop thru all of rowTpls asking them to sync the content they are responsible for if any.
while (rowTpl) {
if (rowTpl.syncContent) {
// *IF* we are selectively updating columns (have been passed changedFieldNames), then pass the column set, else
// pass null, and it will sync all content.
if (rowTpl.syncContent(oldItemDom, newItemDom, changedFieldNames ? columnsToUpdate : null) === false) {
break;
}
}
rowTpl = rowTpl.nextTpl;
}
}
// No custom renderers found in columns to be updated, we can simply update the existing cells.
else {
// Loop through columns which need updating.
for (i = 0, len = columnsToUpdate.length; i < len; i++) {
column = columnsToUpdate[i];
// The dataIndex of the column is the field name
fieldName = column.dataIndex;
value = record.get(fieldName);
cell = Ext.fly(oldItemDom).down(column.getCellSelector(), true);
// Mark the field's dirty status if we are configured to do so (defaults to true)
if (!clearDirty && markDirty) {
cellFly.attach(cell);
if (record.isModified(column.dataIndex)) {
cellFly.addCls(dirtyCls);
} else {
cellFly.removeCls(dirtyCls);
}
}
defaultRenderer = column.usingDefaultRenderer;
scope = defaultRenderer ? column : column.scope;
// Call the column updater which gets passed the TD element
if (column.updater) {
Ext.callback(column.updater, scope, [cell, value, record, me, me.dataSource], 0, column, ownerCt);
}
else {
if (column.renderer) {
value = Ext.callback(column.renderer, scope,
[value, null, record, 0, 0, me.dataSource, me], 0, column, ownerCt);
}
emptyValue = value == null || value === '';
value = emptyValue ? column.emptyCellText : value;
// Update the value of the cell's inner in the best way.
// We only use innerHTML of the cell's inner DIV if the renderer produces HTML
// Otherwise we change the value of the single text node within the inner DIV
// The emptyValue may be HTML, typically defaults to &#160;
if (column.producesHTML || emptyValue) {
cell.childNodes[0].innerHTML = value;
} else {
cell.childNodes[0].childNodes[0].data = value;
}
}
// Add the highlight class if there is one
if (me.highlightClass) {
Ext.fly(cell).addCls(me.highlightClass);
// Start up a DelayedTask which will purge the changedCells stack, removing the highlight class
// after the expiration time
if (!me.changedCells) {
me.self.prototype.changedCells = [];
me.prototype.clearChangedTask = new Ext.util.DelayedTask(me.clearChangedCells, me.prototype);
me.clearChangedTask.delay(me.unhighlightDelay);
}
// Post a changed cell to the stack along with expiration time
me.changedCells.push({
cell: cell,
cls: me.highlightClass,
expires: Ext.Date.now() + 1000
});
}
}
}
// If we have a commit or a reject, some fields may no longer be dirty but may
// not appear in the modified field names. Remove all the dirty class here to be sure.
if (clearDirty && markDirty && !record.dirty) {
Ext.fly(oldItemDom, '_internal').select('.' + dirtyCls).removeCls(dirtyCls);
}
// Coalesce any layouts which happen due to any itemupdate handlers (eg Widget columns) with the final refreshSize layout.
if (hasVariableRowHeight) {
Ext.suspendLayouts();
}
// Since we don't actually replace the row, we need to fire the event with the old row
// because it's the thing that is still in the DOM
me.fireEvent('itemupdate', record, me.store.indexOf(record), oldItemDom);
// We only need to update the layout if any of the columns can change the row height.
if (hasVariableRowHeight) {
if (me.bufferedRenderer) {
me.bufferedRenderer.refreshSize();
// Must climb to ownerGrid in case we've only updated one field in one side of a lockable assembly.
// ownerGrid is always the topmost GridPanel.
me.ownerGrid.updateLayout();
} else {
me.refreshSize();
}
// Ensure any layouts queued by itemupdate handlers and/or the refreshSize call are executed.
Ext.resumeLayouts(true);
}
}
}
},
clearChangedCells: function() {
var me = this,
now = Ext.Date.now(),
changedCell;
for (var i = 0, len = me.changedCells.length; i < len; ) {
changedCell = me.changedCells[i];
if (changedCell.expires <= now) {
Ext.fly(changedCell.cell).removeCls(changedCell.highlightClass);
Ext.Array.erase(me.changedCells, i, 1);
len--;
} else {
break;
}
}
// Keep repeating the delay until all highlighted cells have been cleared
if (len) {
me.clearChangedTask.delay(me.unhighlightDelay);
}
},
updateColumns: function(oldRowDom, newRowDom, columnsToUpdate) {
var me = this,
newAttrs, attLen, attName, attrIndex,
colCount = columnsToUpdate.length,
colIndex,
column,
oldCell, newCell,
cellSelector = me.getCellSelector();
// Copy new row attributes across. Use IE-specific method if possible.
// Must do again at this level because the row DOM passed here may be the nested row in a row wrap.
if (oldRowDom.mergeAttributes) {
oldRowDom.mergeAttributes(newRowDom, true);
} else {
newAttrs = newRowDom.attributes;
attLen = newAttrs.length;
for (attrIndex = 0; attrIndex < attLen; attrIndex++) {
attName = newAttrs[attrIndex].name;
if (attName !== 'id') {
oldRowDom.setAttribute(attName, newAttrs[attrIndex].value);
}
}
}
// Replace changed cells in the existing row structure with the new version from the rendered row.
for (colIndex = 0; colIndex < colCount; colIndex++) {
column = columnsToUpdate[colIndex];
// Pluck out cells using the column's unique cell selector.
// Becuse in a wrapped row, there may be several TD elements.
cellSelector = me.getCellSelector(column);
oldCell = Ext.fly(oldRowDom).selectNode(cellSelector);
newCell = Ext.fly(newRowDom).selectNode(cellSelector);
// Carefully replace just the *contents* of the cell.
Ext.fly(oldCell).syncContent(newCell);
}
},
/**
* @private
* Decides whether the column needs updating
* @return {Number} 0 = Doesn't need update.
* 1 = Column needs update, and renderer has > 1 argument; We need to render a whole new HTML item.
* 2 = Column needs update, but renderer has 1 argument or column uses an updater.
*/
shouldUpdateCell: function(record, column, changedFieldNames) {
// We should not update certain columns (widget column)
if (!column.preventUpdate) {
// The passed column has a renderer which peeks and pokes at other data.
// Return 1 which means that a whole new TableView item must be rendered.
if (column.hasCustomRenderer) {
return 1;
}
// If there is a changed field list, and it's NOT a custom column renderer
// (meaning it doesn't peek at other data, but just uses the raw field value)
// We only have to update it if the column's field is amobg those changes.
if (changedFieldNames) {
var len = changedFieldNames.length,
i, field;
for (i = 0; i < len; ++i) {
field = changedFieldNames[i];
if (field === column.dataIndex || field === record.idProperty) {
return 2;
}
}
} else {
return 2;
}
}
return 0;
},
/**
* Refreshes the grid view. Sets the sort state and focuses the previously focused row.
*/
refresh: function() {
var me = this,
scroller;
me.callParent(arguments);
me.headerCt.setSortState();
// Create horizontal stretcher element if no records in view and there is overflow of the header container.
// Element will be transient and destroyed by the next refresh.
if (me.touchScroll && me.el && !me.all.getCount() && me.headerCt && me.headerCt.tooNarrow) {
scroller = me.getScrollable();
if (scroller) {
scroller.setSize({
x: me.headerCt.getTableWidth(),
y: scroller.getSize().y
});
}
}
},
processContainerEvent: function(e) {
// If we find a component & it belongs to our grid, don't fire the event.
// For example, grid editors resolve to the parent grid
var cmp = Ext.Component.fromElement(e.target.parentNode);
if (cmp && cmp.up(this.ownerCt)) {
return false;
}
},
processItemEvent: function(record, item, rowIndex, e) {
var me = this,
self = me.self,
map = self.EventMap,
type = e.type,
features = me.features,
len = features.length,
i, cellIndex, result, feature, column,
navModel = me.getNavigationModel(),
eventPosition = e.position = me.eventPosition || (me.eventPosition = new Ext.grid.CellContext()),
focusPosition, row, cell;
// IE has a bug whereby if you mousedown in a cell editor in one side of a locking grid and then
// drag out of that, and mouseup in *the other side*, the mousedowned side still receives the event!
// Even though the mouseup target is *not* within it! Ignore the mouseup in this case.
if (Ext.isIE && type === 'mouseup' && !e.within(me.el)) {
return false;
}
// Only process the event if it occurred within an item which maps to a record in the store
if (me.indexInStore(item) !== -1) {
row = eventPosition.rowElement = Ext.fly(item).down(me.rowSelector, true);
// For key events, pull context from NavigationModel
if (Ext.String.startsWith(e.type, 'key') && (focusPosition = navModel.getPosition())) {
cell = (cell = navModel.getCell()) && cell.dom;
column = focusPosition.column;
}
// Even with a NavigationModel, synthetic click events might be recieved before focus
// is received, so attempt to access the cell from the target.
if (!cell) {
cell = e.getTarget(me.getCellSelector(), row);
}
type = self.TouchEventMap[type] || type;
if (cell) {
if (!cell.parentNode) {
// If we have no parentNode, the td has been removed from the DOM, probably via an update,
// so just jump out since the target for the event isn't valid
return false;
}
if (!column) {
column = me.getHeaderByCell(cell);
}
// Find the index of the header in the *full* (including hidden columns) leaf column set.
// Because In 4.0.0 we rendered hidden cells, and the cellIndex included the hidden ones.
cellIndex = me.ownerCt.getColumnManager().getHeaderIndex(column);
} else {
cellIndex = -1;
}
eventPosition.setAll(
me,
rowIndex,
column ? me.getVisibleColumnManager().getHeaderIndex(column) : -1,
record,
column
);
eventPosition.cellElement = cell;
result = me.fireEvent('uievent', type, me, cell, rowIndex, cellIndex, e, record, row);
// If the event has been stopped by a handler, tell the selModel (if it is interested) and return early.
// For example, action columns by default will stop event propagation by returning `false` from its
// 'uievent' event handler.
if ((result === false || me.callParent(arguments) === false)) {
return false;
}
for (i = 0; i < len; ++i) {
feature = features[i];
// In some features, the first/last row might be wrapped to contain extra info,
// such as grouping or summary, so we may need to stop the event
if (feature.wrapsItem) {
if (feature.vetoEvent(record, row, rowIndex, e) === false) {
// If the feature is vetoing the event, there's a good chance that
// it's for some feature action in the wrapped row.
me.processSpecialEvent(e);
// Prevent focus/selection here until proper focus handling is added for non-data rows
// This should probably be removed once this is implemented.
e.preventDefault();
return false;
}
}
}
// if the element whose event is being processed is not an actual cell (for example if using a rowbody
// feature and the rowbody element's event is being processed) then do not fire any "cell" events
// Don't handle cellmouseenter and cellmouseleave events for now
if (cell && type !== 'mouseover' && type !== 'mouseout') {
result = !(
// We are adding cell and feature events
(me['onBeforeCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
(me.fireEvent('beforecell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false) ||
(me['onCell' + map[type]](cell, cellIndex, record, row, rowIndex, e) === false) ||
(me.fireEvent('cell' + type, me, cell, cellIndex, record, row, rowIndex, e) === false)
);
}
if (result !== false) {
result = me.fireEvent('row' + type, me, record, row, rowIndex, e);
}
return result;
} else {
// If it's not in the store, it could be a feature event, so check here
this.processSpecialEvent(e);
// Prevent focus/selection here until proper focus handling is added for non-data rows
// This should probably be removed once this is implemented.
e.preventDefault();
return false;
}
},
processSpecialEvent: function(e) {
var me = this,
features = me.features,
ln = features.length,
type = e.type,
i, feature, prefix, featureTarget,
beforeArgs, args,
panel = me.ownerCt;
me.callParent(arguments);
if (type === 'mouseover' || type === 'mouseout') {
return;
}
type = me.self.TouchEventMap[type] || type;
for (i = 0; i < ln; i++) {
feature = features[i];
if (feature.hasFeatureEvent) {
featureTarget = e.getTarget(feature.eventSelector, me.getTargetEl());
if (featureTarget) {
prefix = feature.eventPrefix;
// allows features to implement getFireEventArgs to change the
// fireEvent signature
beforeArgs = feature.getFireEventArgs('before' + prefix + type, me, featureTarget, e);
args = feature.getFireEventArgs(prefix + type, me, featureTarget, e);
if (
// before view event
(me.fireEvent.apply(me, beforeArgs) === false) ||
// panel grid event
(panel.fireEvent.apply(panel, beforeArgs) === false) ||
// view event
(me.fireEvent.apply(me, args) === false) ||
// panel event
(panel.fireEvent.apply(panel, args) === false)
) {
return false;
}
}
}
}
return true;
},
onCellMouseDown: Ext.emptyFn,
onCellLongPress: Ext.emptyFn,
onCellMouseUp: Ext.emptyFn,
onCellClick: Ext.emptyFn,
onCellDblClick: Ext.emptyFn,
onCellContextMenu: Ext.emptyFn,
onCellKeyDown: Ext.emptyFn,
onCellKeyUp: Ext.emptyFn,
onCellKeyPress: Ext.emptyFn,
onBeforeCellMouseDown: Ext.emptyFn,
onBeforeCellLongPress: Ext.emptyFn,
onBeforeCellMouseUp: Ext.emptyFn,
onBeforeCellClick: Ext.emptyFn,
onBeforeCellDblClick: Ext.emptyFn,
onBeforeCellContextMenu: Ext.emptyFn,
onBeforeCellKeyDown: Ext.emptyFn,
onBeforeCellKeyUp: Ext.emptyFn,
onBeforeCellKeyPress: Ext.emptyFn,
/**
* Expands a particular header to fit the max content width.
* @deprecated Use {@link #autoSizeColumn} instead.
*/
expandToFit: function(header) {
this.autoSizeColumn(header);
},
/**
* Sizes the passed header to fit the max content width.
* *Note that group columns shrinkwrap around the size of leaf columns. Auto sizing a group column
* autosizes descendant leaf columns.*
* @param {Ext.grid.column.Column/Number} header The header (or index of header) to auto size.
*/
autoSizeColumn: function(header) {
if (Ext.isNumber(header)) {
header = this.getGridColumns()[header];
}
if (header) {
if (header.isGroupHeader) {
header.autoSize();
return;
}
delete header.flex;
header.setWidth(this.getMaxContentWidth(header));
}
},
/**
* Returns the max contentWidth of the header's text and all cells
* in the grid under this header.
* @private
*/
getMaxContentWidth: function(header) {
var me = this,
cells = me.el.query(header.getCellInnerSelector()),
originalWidth = header.getWidth(),
i = 0,
ln = cells.length,
columnSizer = me.body.select(me.getColumnSizerSelector(header)),
max = Math.max,
widthAdjust = 0,
maxWidth;
if (ln > 0) {
if (Ext.supports.ScrollWidthInlinePaddingBug) {
widthAdjust += me.getCellPaddingAfter(cells[0]);
}
if (me.columnLines) {
widthAdjust += Ext.fly(cells[0].parentNode).getBorderWidth('lr');
}
}
// Set column width to 1px so we can detect the content width by measuring scrollWidth
columnSizer.setWidth(1);
// We are about to measure the offsetWidth of the textEl to determine how much
// space the text occupies, but it will not report the correct width if the titleEl
// has text-overflow:ellipsis. Set text-overflow to 'clip' before proceeding to
// ensure we get the correct measurement.
header.textEl.setStyle({
"text-overflow": 'clip',
display: 'table-cell'
});
// Allow for padding round text of header
maxWidth = header.textEl.dom.offsetWidth + header.titleEl.getPadding('lr');
// revert to using text-overflow defined by the stylesheet
header.textEl.setStyle({
"text-overflow": '',
display: ''
});
for (; i < ln; i++) {
maxWidth = max(maxWidth, cells[i].scrollWidth);
}
// in some browsers, the "after" padding is not accounted for in the scrollWidth
maxWidth += widthAdjust;
// 40 is the minimum column width. TODO: should this be configurable?
// One extra pixel needed. EXACT width shrinkwrap of text causes ellipsis to appear.
maxWidth = max(maxWidth + 1, 40);
// Set column width back to original width
columnSizer.setWidth(originalWidth);
return maxWidth;
},
getPositionByEvent: function(e) {
var me = this,
cellNode = e.getTarget(me.cellSelector),
rowNode = e.getTarget(me.itemSelector),
record = me.getRecord(rowNode),
header = me.getHeaderByCell(cellNode);
return me.getPosition(record, header);
},
getHeaderByCell: function(cell) {
if (cell) {
return this.ownerCt.getVisibleColumnManager().getHeaderById(cell.getAttribute('data-columnId'));
}
return false;
},
/**
* @param {Object} position The current row and column: an object containing the following properties:
*
* - row - The row index
* - column - The column index
*
* @param {String} direction 'up', 'down', 'right' and 'left'
* @param {Ext.event.Event} e event
* @param {Boolean} preventWrap Set to true to prevent wrap around to the next or previous row.
* @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.
* @param {Object} scope Scope to run the verifierFn in
* @return {Ext.grid.CellContext} An object encapsulating the unique cell position.
*
* @private
*/
walkCells: function(pos, direction, e, preventWrap, verifierFn, scope) {
// Caller (probably CellModel) had no current position. This can happen
// if the main el is focused and any navigation key is presssed.
if (!pos) {
return false;
}
var me = this,
// Use original, documented row/column properties if passed.
// The object should be a CellContext with rowIdx & colIdx
row = typeof pos.row === 'number' ? pos.row : pos.rowIdx,
column = typeof pos.column === 'number' ? pos.column : pos.colIdx,
rowCount = me.dataSource.getCount(),
columns = me.ownerCt.getVisibleColumnManager(),
firstIndex = columns.getHeaderIndex(columns.getFirst()),
lastIndex = columns.getHeaderIndex(columns.getLast()),
newRow = row,
newColumn = column,
activeHeader = columns.getHeaderAtIndex(column);
// no active header or its currently hidden
if (!activeHeader || activeHeader.hidden || !rowCount) {
return false;
}
e = e || {};
direction = direction.toLowerCase();
switch (direction) {
case 'right':
// has the potential to wrap if its last
if (column === lastIndex) {
// if bottom row and last column, deny right
if (preventWrap || row === rowCount - 1) {
return false;
}
if (!e.ctrlKey) {
// otherwise wrap to nextRow and firstCol
newRow = me.walkRows(row, 1);
if (newRow !== row) {
newColumn = firstIndex;
}
}
// go right
} else {
if (!e.ctrlKey) {
newColumn = columns.getHeaderIndex(columns.getNextSibling(activeHeader));
} else {
newColumn = lastIndex;
}
}
break;
case 'left':
// has the potential to wrap
if (column === firstIndex) {
// if top row and first column, deny left
if (preventWrap || row === 0) {
return false;
}
if (!e.ctrlKey) {
// otherwise wrap to prevRow and lastIndex
newRow = me.walkRows(row, -1);
if (newRow !== row) {
newColumn = lastIndex;
}
}
// go left
} else {
if (!e.ctrlKey) {
newColumn = columns.getHeaderIndex(columns.getPreviousSibling(activeHeader));
} else {
newColumn = firstIndex;
}
}
break;
case 'up':
// if top row, deny up
if (row === 0) {
return false;
// go up
} else {
if (!e.ctrlKey) {
newRow = me.walkRows(row, -1);
} else {
// Go to first row by walking down from row -1
newRow = me.walkRows(-1, 1);
}
}
break;
case 'down':
// if bottom row, deny down
if (row === rowCount - 1) {
return false;
// go down
} else {
if (!e.ctrlKey) {
newRow = me.walkRows(row, 1);
} else {
// Go to first row by walking up from beyond the last row
newRow = me.walkRows(rowCount, -1);
}
}
break;
}
if (verifierFn && verifierFn.call(scope || me, {row: newRow, column: newColumn}) !== true) {
return false;
}
newColumn = columns.getHeaderAtIndex(newColumn);
return new Ext.grid.CellContext(me).setPosition(newRow, newColumn);
},
/**
* Increments the passed row index by the passed increment which may be +ve or -ve
*
* Skips hidden rows.
*
* If no row is visible in the specified direction, returns the input row index unchanged.
* @param {Number} startRow The zero-based row index to start from.
* @param {Number} distance The distance to move the row by. May be +ve or -ve.
*/
walkRows: function(startRow, distance) {
// Note that we use the **dataSource** here because row indices mean view row indices
// so records in collapsed groups must be omitted.
var me = this,
store = me.dataSource,
moved = 0,
lastValid = startRow,
node,
limit = (distance < 0) ? 0 : (store.isBufferedStore ? store.getTotalCount() : store.getCount()) - 1,
increment = limit ? 1 : -1,
result = startRow;
do {
// Walked off the end: return the last encountered valid row
if (limit ? result >= limit : result <= limit) {
return lastValid || limit;
}
// Move the result pointer on by one position. We have to count intervening VISIBLE nodes
result += increment;
// Stepped onto VISIBLE record: Increment the moved count.
// We must not count stepping onto a non-rendered record as a move.
if ((node = Ext.fly(me.getRow(result))) && node.isVisible(true)) {
moved += increment;
lastValid = result;
}
} while (moved !== distance);
return result;
},
/**
* Navigates from the passed record by the passed increment which may be +ve or -ve
*
* Skips hidden records.
*
* If no record is visible in the specified direction, returns the starting record index unchanged.
* @param {Ext.data.Model} startRec The Record to start from.
* @param {Number} distance The distance to move from the record. May be +ve or -ve.
*/
walkRecs: function(startRec, distance) {
// Note that we use the **store** to access the records by index because the dataSource omits records in collapsed groups.
// This is used by selection models which use the **store**
var me = this,
store = me.dataSource,
moved = 0,
lastValid = startRec,
node,
limit = (distance < 0) ? 0 : (store.isBufferedStore ? store.getTotalCount() : store.getCount()) - 1,
increment = limit ? 1 : -1,
testIndex = store.indexOf(startRec),
rec;
do {
// Walked off the end: return the last encountered valid record
if (limit ? testIndex >= limit : testIndex <= limit) {
return lastValid;
}
// Move the result pointer on by one position. We have to count intervening VISIBLE nodes
testIndex += increment;
// Stepped onto VISIBLE record: Increment the moved count.
// We must not count stepping onto a non-rendered record as a move.
rec = store.getAt(testIndex);
if (!rec.isCollapsedPlaceholder && (node = Ext.fly(me.getNodeByRecord(rec))) && node.isVisible(true)) {
moved += increment;
lastValid = rec;
}
} while (moved !== distance);
return lastValid;
},
/**
* Returns the index of the first row in your table view deemed to be visible.
* @return {Number}
* @private
*/
getFirstVisibleRowIndex: function() {
var me = this,
count = (me.dataSource.isBufferedStore ? me.dataSource.getTotalCount() : me.dataSource.getCount()),
result = me.indexOf(me.all.first()) - 1;
do {
result += 1;
if (result === count) {
return;
}
} while (!Ext.fly(me.getRow(result)).isVisible(true));
return result;
},
/**
* Returns the index of the last row in your table view deemed to be visible.
* @return {Number}
* @private
*/
getLastVisibleRowIndex: function() {
var me = this,
result = me.indexOf(me.all.last());
do {
result -= 1;
if (result === -1) {
return;
}
} while (!Ext.fly(me.getRow(result)).isVisible(true));
return result;
},
getHeaderCt: function() {
return this.headerCt;
},
getPosition: function(record, header) {
return new Ext.grid.CellContext(this).setPosition(record, header);
},
onDestroy: function() {
var me = this,
features = me.featuresMC,
len,
i;
if (features) {
for (i = 0, len = features.getCount(); i < len; ++i) {
features.getAt(i).destroy();
}
}
me.cellFly = me.featuresMC = null;
me.callParent(arguments);
},
// Private.
// Respond to store replace event which is fired by GroupStore group expand/collapse operations.
// This saves a layout because a remove and add operation are coalesced in this operation.
onReplace: function(store, startIndex, oldRecords, newRecords) {
var me = this,
bufferedRenderer = me.bufferedRenderer;
// If there's a buffered renderer and the removal range falls inside the current view...
if (me.rendered && bufferedRenderer) {
bufferedRenderer.onReplace(store, startIndex, oldRecords, newRecords);
} else {
me.callParent(arguments);
}
me.setPendingStripe(startIndex);
},
// after adding a row stripe rows from then on
onAdd: function(store, records, index) {
var me = this,
bufferedRenderer = me.bufferedRenderer;
if (me.rendered && bufferedRenderer) {
bufferedRenderer.onReplace(store, index, [], records);
} else {
me.callParent(arguments);
}
me.setPendingStripe(index);
},
// after removing a row stripe rows from then on
onRemove: function(store, records, index) {
var me = this,
bufferedRenderer = me.bufferedRenderer;
// If there's a BufferedRenderer...
if (me.rendered && bufferedRenderer) {
bufferedRenderer.onReplace(store, index, records, []);
} else {
me.callParent(arguments);
}
me.setPendingStripe(index);
},
// When there's a buffered renderer present, store refresh events cause TableViews to go to scrollTop:0
onDataRefresh: function() {
var me = this,
owner = me.ownerCt;
// If triggered during an animation, refresh once we're done
if (owner && owner.isCollapsingOrExpanding === 2) {
owner.on('expand', me.onDataRefresh, me, {single: true});
return;
}
me.callParent();
},
getViewRange: function() {
var me = this;
if (me.bufferedRenderer) {
return me.bufferedRenderer.getViewRange();
}
return me.callParent();
},
setPendingStripe: function(index) {
var current = this.stripeOnUpdate;
if (current === null) {
current = index;
} else {
current = Math.min(current, index);
}
this.stripeOnUpdate = current;
},
onEndUpdate: function() {
var me = this,
stripeOnUpdate = me.stripeOnUpdate,
startIndex = me.all.startIndex;
if (me.rendered && (stripeOnUpdate || stripeOnUpdate === 0)) {
if (stripeOnUpdate < startIndex) {
stripeOnUpdate = startIndex;
}
me.doStripeRows(stripeOnUpdate);
me.stripeOnUpdate = null;
}
me.callParent(arguments);
},
/**
* Stripes rows from a particular row index.
* @param {Number} startRow
* @param {Number} [endRow] argument specifying the last row to process.
* By default process up to the last row.
* @private
*/
doStripeRows: function(startRow, endRow) {
var me = this,
rows,
rowsLn,
i,
row;
// ensure stripeRows configuration is turned on
if (me.rendered && me.stripeRows) {
rows = me.getNodes(startRow, endRow);
for (i = 0, rowsLn = rows.length; i < rowsLn; i++) {
row = rows[i];
// Remove prior applied row classes.
row.className = row.className.replace(me.rowClsRe, ' ');
startRow++;
// Every odd row will get an additional cls
if (startRow % 2 === 0) {
row.className += (' ' + me.altRowCls);
}
}
}
},
hasActiveFeature: function(){
return (this.isGrouping && this.store.isGrouped()) || this.isRowWrapped;
},
getCellPaddingAfter: function(cell) {
return Ext.fly(cell).getPadding('r');
},
privates: {
refreshScroll: function () {
var me = this,
bufferedRenderer = me.bufferedRenderer;
// If there is a BufferedRenderer, we must refresh the scroller using BufferedRenderer methods
// which take account of the full virtual scroll range.
if (bufferedRenderer) {
bufferedRenderer.refreshSize();
} else {
me.callParent();
}
}
}
});