/**
* @class Ext.grid.GridView
* @extends Ext.misc.AbstractGridView
* This class encapsulates the user interface of an {@link Ext.grid.GridPanel}.
* Methods of this class may be used to access user interface elements to enable
* special display effects. Do not change the DOM structure of the user interface.
* This class does not provide ways to manipulate the underlying data. The data
* model of a Grid is held in an {@link Ext.data.Store}.
* @constructor
* @param {Object} config
*/
Ext.grid.GridView = Ext.extend(Ext.misc.AbstractGridView, {
/**
* Override this function to apply custom CSS classes to rows during rendering. You can also supply custom
* parameters to the row template for the current row to customize how it is rendered using the rowParams
* parameter. This function should return the CSS class name (or empty string '' for none) that will be added
* to the row's wrapping div. To apply multiple class names, simply return them space-delimited within the string
* (e.g., 'my-class another-class'). Example usage:
viewConfig: {
forceFit: true,
showPreview: true, // custom property
enableRowBody: true, // required to create a second, full-width row to show expanded Record data
getRowClass: function(record, rowIndex, rp, ds){ // rp = rowParams
if(this.showPreview){
rp.body = '<p>'+record.data.excerpt+'</p>';
return 'x-grid3-row-expanded';
}
return 'x-grid3-row-collapsed';
}
},
* @param {Record} record The {@link Ext.data.Record} corresponding to the current row.
* @param {Number} index The row index.
* @param {Object} rowParams A config object that is passed to the row template during rendering that allows
* customization of various aspects of a grid row.
* If {@link #enableRowBody} is configured true, then the following properties may be set
* by this function, and will be used to render a full-width expansion row below each grid row:
*
* The following property will be passed in, and may be appended to:
*
* @param {Store} store The {@link Ext.data.Store} this grid is bound to
* @method getRowClass
* @return {String} a CSS class name to add to the row.
*/
/**
* @cfg {Boolean} enableRowBody True to add a second TR element per row that can be used to provide a row body
* that spans beneath the data row. Use the {@link #getRowClass} method's rowParams config to customize the row body.
*/
cellCls: 'x-grid3-cell',
firstCellCls: 'x-grid3-cell-first',
lastCellCls: 'x-grid3-cell-last',
dirtyCellCls: 'x-grid3-dirty-cell',
selectedCellCls: 'x-grid3-cell-selected',
focusedCellCls: 'x-grid3-cell-focus',
rowCls: 'x-grid3-row',
firstRowCls: 'x-grid3-row-first',
lastRowCls: 'x-grid3-row-last',
dirtyRowCls: 'x-grid3-dirty-row',
altRowCls: 'x-grid3-row-alt',
rowOverCls: 'x-grid3-row-over',
selectedRowCls: 'x-grid3-row-selected',
focusedRowCls: 'x-grid3-row-focus',
rowBodyCls: 'x-grid3-row-body',
// private
rowClsRe: /(?:^|\s+)x-grid3-row-(first|last|alt)(?:\s+|$)/g,
/**
* @cfg {String} rowBodySelector The selector used to find row bodies internally (defaults to 'div.x-grid3-row')
*/
rowBodySelectorDepth: 10,
buildSelectors: function() {
Ext.grid.GridView.superclass.buildSelectors.call(this);
this.rowBodySelector = '.' + this.rowBodyCls;
},
/* -------------------------------- UI Specific ----------------------------- */
/**
* The template to use when rendering the body. Has a default template
* @property bodyTpl
* @type Ext.Template
*/
bodyTpl: new Ext.Template('{rows}'),
/**
* The template to use to render each cell. Has a default template
* @property cellTpl
* @type Ext.Template
*/
cellTpl: new Ext.Template(
'',
' {value} ',
' | '
),
/**
* @private
* Provides default templates if they are not given for this particular instance. Most of the templates are defined on
* the prototype, the ones defined inside this function are done so because they are based on Grid or GridView configuration
*/
initTemplates : function() {
var templates = this.templates || {},
template, name,
rowBodyText = [
'',
'',
' {body} ',
' | ',
'
'
].join(""),
innerText = [
'',
'',
'{cells}
',
this.enableRowBody ? rowBodyText : '',
'',
'
'
].join("");
if (!templates.master) {
templates.master = new Ext.Template(
''
);
}
Ext.applyIf(templates, {
cell : this.cellTpl,
body : this.bodyTpl,
row : new Ext.Template('' + innerText + '
'),
rowInner: new Ext.Template(innerText)
});
for (name in templates) {
template = templates[name];
if (template && Ext.isFunction(template.compile) && !template.compiled) {
template.disableFormats = true;
template.compile();
}
}
this.templates = templates;
// Provides a matching group to lookup the column of a particular
// cell in the Grid.
this.colRe = new RegExp('x-grid3-td-([^\\s]+)', '');
},
// Get the columnIndex within an element
getCellIndex : function(el) {
if (el) {
var match = el.className.match(this.colRe);
if (match && match[1]) {
return this.grid.colModel.getIndexById(match[1]);
}
}
return false;
},
/**
* Return the HtmlElement representing the grid row body which contains the passed element.
* @param {HTMLElement} el The target HTMLElement
* @return {HTMLElement} The row body element, or null if the target element is not within a row body of this GridView.
*/
findRowBody : function(el) {
if (!el) {
return false;
}
return this.fly(el).findParent(this.rowBodySelector, this.rowBodySelectorDepth);
},
// getter methods for fetching elements dynamically in the grid
/**
* @private
* Called after a column's width has been updated, this resizes all of the cells for that column in each row
* @param {Number} column The column index
*/
updateColumnWidth : function(column, width) {
var columnWidth = this.getColumnWidth(column),
totalWidth = this.getTotalWidth(),
nodes = this.getRows(),
nodeCount = nodes.length,
row,
i = 0,
firstChild;
this.updateHeaderWidth();
for (; i < nodeCount; i++) {
row = nodes[i];
firstChild = row.firstChild;
row.style.width = totalWidth;
if (firstChild) {
firstChild.style.width = totalWidth;
firstChild.rows[0].childNodes[column].style.width = columnWidth;
}
}
this.onColumnWidthUpdated(column, columnWidth, totalWidth);
},
/**
* @private
* Sets the hidden status of a given column.
* @param {Number} col The column index
* @param {Boolean} hidden True to make the column hidden
*/
updateColumnHidden : function(col, hidden) {
var totalWidth = this.getTotalWidth(),
display = hidden ? 'none' : '',
rows = this.getRows(),
rowLength = rows.length,
row,
rowFirstChild,
i = 0;
for (; i < rowLength; i++) {
row = rows[i];
row.style.width = totalWidth;
rowFirstChild = row.firstChild;
if (rowFirstChild) {
rowFirstChild.style.width = totalWidth;
rowFirstChild.rows[0].childNodes[col].style.display = display;
}
}
this.onColumnHiddenUpdated(col, hidden, totalWidth);
},
/**
* @private
* Renders all of the rows to a string buffer and returns the string. This is called internally
* by renderRows and performs the actual string building for the rows - it does not inject HTML into the DOM.
* @param {Array} columns The column data acquired from getColumnData.
* @param {Array} records The array of records to render
* @param {Ext.data.Store} store The store to render the rows from
* @param {Number} startRow The index of the first row being rendered. Sometimes we only render a subset of
* the rows so this is used to maintain logic for striping etc
* @param {Number} colCount The total number of columns in the column model
* @param {Boolean} stripe True to stripe the rows
* @return {String} A string containing the HTML for the rendered rows
*/
doRender : function(columns, records, store, startRow, colCount, stripe) {
var templates = this.templates,
cellTemplate = templates.cell,
rowTemplate = templates.row,
last = colCount - 1,
tstyle = 'width:' + this.getTotalWidth() + ';',
// buffers
rowBuffer = [],
colBuffer = [],
rowParams = {tstyle: tstyle},
meta = {},
len = records.length,
alt,
column,
record,
i, j, rowIndex;
//build up each row's HTML
for (j = 0; j < len; j++) {
record = records[j];
colBuffer = [];
rowIndex = j + startRow;
//build up each column's HTML
for (i = 0; i < colCount; i++) {
column = columns[i];
meta.id = column.id;
meta.css = i === 0 ? (this.firstCellCls + ' ') : (i === last ? (this.lastCellCls + ' ') : '');
meta.attr = meta.cellAttr = '';
meta.style = column.style;
meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store);
if (Ext.isEmpty(meta.value)) {
meta.value = ' ';
}
if (this.markDirty && record.dirty && typeof record.modified[column.name] !== 'undefined') {
meta.css += ' ' + this.dirtyCellCls;
}
colBuffer[colBuffer.length] = cellTemplate.apply(meta);
}
alt = [];
//set up row striping and row dirtiness CSS classes
if (stripe && ((rowIndex + 1) % 2 === 0)) {
alt[0] = this.altRowCls;
}
if (record.dirty) {
alt[1] = ' ' + this.dirtyRowCls;
}
rowParams.cols = colCount;
if (this.getRowClass) {
alt[2] = this.getRowClass(record, rowIndex, rowParams, store);
}
rowParams.alt = alt.join(' ');
rowParams.cells = colBuffer.join('');
rowBuffer[rowBuffer.length] = rowTemplate.apply(rowParams);
}
return rowBuffer.join('');
},
// private
processEvent : function(name, e) {
var target = e.getTarget(),
grid = this.grid,
//header = this.findHeaderIndex(target),
row, cell, col, body;
grid.fireEvent(name, e);
//if (header !== false) {
//grid.fireEvent('header' + name, grid, header, e);
//} else {
row = this.findRowIndex(target);
// Grid's value-added events must bubble correctly to allow cancelling via returning false: cell->column->row
// We must allow a return of false at any of these levels to cancel the event processing.
// Particularly allowing rowmousedown to be cancellable by prior handlers which need to prevent selection.
if (row !== false) {
cell = this.findCellIndex(target);
if (cell !== false) {
col = grid.colModel.getColumnAt(cell);
if (grid.fireEvent('cell' + name, grid, row, cell, e) !== false) {
if (!col || (col.processEvent && (col.processEvent(name, e, grid, row, cell) !== false))) {
grid.fireEvent('row' + name, grid, row, e);
}
}
} else {
if (grid.fireEvent('row' + name, grid, row, e) !== false) {
(body = this.findRowBody(target)) && grid.fireEvent('rowbody' + name, grid, row, e);
}
}
} else {
grid.fireEvent('container' + name, grid, e);
}
//}
},
// private
insertRows : function(dm, firstRow, lastRow, isUpdate) {
var last = dm.getCount() - 1;
if (!isUpdate && firstRow === 0 && lastRow >= last) {
this.fireEvent('beforerowsinserted', this, firstRow, lastRow);
this.refresh();
this.fireEvent('rowsinserted', this, firstRow, lastRow);
} else {
if (!isUpdate) {
this.fireEvent('beforerowsinserted', this, firstRow, lastRow);
}
var html = this.renderRows(firstRow, lastRow),
before = this.getRow(firstRow);
if (before) {
if (firstRow === 0) {
this.fly(this.getRow(0)).removeClass(this.firstRowCls);
}
Ext.core.DomHelper.insertHtml('beforeBegin', before, html);
} else {
var r = this.getRow(last - 1);
if (r) {
this.fly(r).removeClass(this.lastRowCls);
}
Ext.core.DomHelper.insertHtml('beforeEnd', this.mainBody.dom, html);
}
if (!isUpdate) {
this.fireEvent('rowsinserted', this, firstRow, lastRow);
this.processRows(firstRow);
} else if (firstRow === 0 || firstRow >= last) {
//ensure first/last row is kept after an update.
this.fly(this.getRow(firstRow)).addClass(firstRow === 0 ? this.firstRowCls : this.lastRowCls);
}
}
this.syncFocusEl(firstRow);
},
/**
* @private
* Refreshes a row by re-rendering it. Fires the rowupdated event when done
*/
refreshRow: function(record) {
var store = this.ds,
colCount = this.grid.colModel.getColumnCount(),
columns = this.getColumnData(),
last = colCount - 1,
cls = [this.rowCls],
rowParams = {
tstyle: String.format("width: {0};", this.getTotalWidth())
},
colBuffer = [],
cellTpl = this.templates.cell,
rowIndex, row, column, meta, css, i;
if (Ext.isNumber(record)) {
rowIndex = record;
record = store.getAt(rowIndex);
} else {
rowIndex = store.indexOf(record);
}
//the record could not be found
if (!record || rowIndex < 0) {
return;
}
//builds each column in this row
for (i = 0; i < colCount; i++) {
column = columns[i];
if (i === 0) {
css = this.firstCellCls;
} else {
css = (i === last) ? (this.lastCellCls + ' ') : '';
}
meta = {
id : column.id,
style : column.style,
css : css,
attr : "",
cellAttr: ""
};
// Need to set this after, because we pass meta to the renderer
meta.value = column.renderer.call(column.scope, record.data[column.name], meta, record, rowIndex, i, store);
if (Ext.isEmpty(meta.value)) {
meta.value = ' ';
}
if (this.markDirty && record.dirty && typeof record.modified[column.name] !== 'undefined') {
meta.css += ' ' + this.dirtyCellCls;
}
colBuffer[i] = cellTpl.apply(meta);
}
row = this.getRow(rowIndex);
row.className = '';
if (this.grid.stripeRows && ((rowIndex + 1) % 2 === 0)) {
cls.push(this.altRowCls);
}
if (this.getRowClass) {
rowParams.cols = colCount;
cls.push(this.getRowClass(record, rowIndex, rowParams, store));
}
this.fly(row).addClass(cls).setStyle(rowParams.tstyle);
rowParams.cells = colBuffer.join("");
row.innerHTML = this.templates.rowInner.apply(rowParams);
this.fireEvent('rowupdated', this, rowIndex, record);
}
});
// private
// This is a support class used internally by the Grid components
Ext.grid.GridView.SplitDragZone = Ext.extend(Ext.dd.DDProxy, {
constructor: function(grid) {
this.grid = grid;
this.view = grid.getView();
this.marker = this.view.resizeMarker;
this.proxy = this.view.resizeProxy;
Ext.grid.GridView.SplitDragZone.superclass.constructor.call(this,
grid.getColumnModel().getEl(),
'gridSplitters' + this.grid.getViewEl().id,
{
dragElId: Ext.id(this.proxy.dom),
resizeFrame: false
}
);
this.scroll = false;
this.hw = this.view.splitHandleWidth || 5;
},
b4StartDrag : function(x, y) {
this.dragHeadersDisabled = this.view.headersDisabled;
this.view.headersDisabled = true;
var h = this.view.mainWrap.getHeight();
this.marker.setHeight(h);
this.marker.show();
this.marker.alignTo(this.view.getHeaderCell(this.cellIndex), 'tl-tl', [-2, 0]);
this.proxy.setHeight(h);
var w = this.grid.colModel.getColumnWidth(this.cellIndex),
minw = Math.max(w - this.grid.minColumnWidth, 0);
this.resetConstraints();
this.setXConstraint(minw, 1000);
this.setYConstraint(0, 0);
this.minX = x - minw;
this.maxX = x + 1000;
this.startPos = x;
Ext.dd.DDProxy.prototype.b4StartDrag.call(this, x, y);
},
allowHeaderDrag : function(e) {
return true;
},
handleMouseDown : function(e) {
var t = this.view.findHeaderCell(e.getTarget());
if (t && this.allowHeaderDrag(e)) {
var xy = this.view.fly(t).getXY(),
x = xy[0],
exy = e.getXY(),
ex = exy[0],
w = t.offsetWidth,
adjust = false;
if ((ex - x) <= this.hw) {
adjust = -1;
} else if ((x + w) - ex <= this.hw) {
adjust = 0;
}
if (adjust !== false) {
this.cm = this.grid.colModel;
var ci = this.view.getCellIndex(t);
if (adjust === -1) {
if (ci + adjust < 0) {
return;
}
while (this.cm.isHidden(ci + adjust)) {
--adjust;
if (ci + adjust < 0) {
return;
}
}
}
this.cellIndex = ci + adjust;
this.split = t.dom;
if (this.cm.isResizable(this.cellIndex) && !this.cm.isFixed(this.cellIndex)) {
Ext.grid.GridView.SplitDragZone.superclass.handleMouseDown.apply(this, arguments);
}
} else if (this.view.columnDrag) {
this.view.columnDrag.callHandleMouseDown(e);
}
}
},
endDrag : function(e) {
this.marker.hide();
var v = this.view,
endX = Math.max(this.minX, e.getPageX()),
diff = endX - this.startPos,
disabled = this.dragHeadersDisabled;
v.onColumnSplitterMoved(this.cellIndex, this.cm.getColumnWidth(this.cellIndex) + diff);
setTimeout(function() {
v.headersDisabled = disabled;
}, 50);
},
autoOffset : function() {
this.setDelta(0, 0);
}
});