icloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsapp
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
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   |
|
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(); |
|
} |
|
} |
|
} |
|
});
|
|
|