/**
* Instances of this class encapsulate a position in a grid's row/column coordinate system.
*
* Cells are addressed using the owning {@link #record} and {@link #column} for robustness.
* the column may be moved, the store may be sorted, and the CellContext will still reference
* the same *logical* cell. Be aware that due to buffered rendering the *physical* cell may not
* exist.
*
* The {@link #setPosition} method however allows a numeric row and column to be passed in. These
* are immediately converted.
*
* Be careful not to make `CellContext` objects *too* persistent. If the owning record is removed,
* or the owning column is removed, the reference will be stale.
*
* Freshly created context objects, such as those exposed by events from the
* {@link Ext.grid.selection.SpreadsheetModel spreadsheet selection model} are safe to use
* until your application mutates the store, or changes the column set.
*/
Ext.define('Ext.grid.CellContext', {
/**
* @property {Boolean} isCellContext
* @readonly
* `true` in this class to identify an object as an instantiated CellContext,
* or subclass thereof.
*/
isCellContext: true,
/**
* @readonly
* @property {Ext.grid.column.Column} column
* The grid column which owns the referenced cell.
*/
/**
* @readonly
* @property {Ext.data.Model} record
* The store record which maps to the referenced cell.
*/
/**
* @readonly
* @property {Number} rowIdx
* The row number in the store which owns the referenced cell.
*
* *Be aware that after the initial call to {@link #setPosition}, this value may become stale
* due to subsequent store mutation.*
*/
/**
* @readonly
* @property {Number} colIdx
* The column index in the owning View's leaf column set of the referenced cell.
*
* *Be aware that after the initial call to {@link #setPosition}, this value may become stale
* due to subsequent column mutation.*
*/
generation: 0,
/**
* Creates a new CellContext which references a {@link Ext.view.Table GridView}
* @param {Ext.view.Table} view The {@link Ext.view.Table GridView} for which the cell context
* is needed.
*
* To complete creation of a valid context, use the {@link #setPosition} method.
*/
constructor: function(view) {
this.view = view;
},
/**
* Binds this cell context to a logical cell defined by the {@link #record} and {@link #column}.
*
* @param {Number/Ext.data.Model} row The row index or record which owns the required cell.
* @param {Number/Ext.grid.column.Column} col The column index (In the owning View's leaf
* column set), or the owning {@link Ext.grid.column.Column column}.
*
* A one argument form may be used in the form of an array:
*
* [column, row]
*
* Or another CellContext may be passed.
*
* @return {Ext.grid.CellContext} this CellContext object.
*/
setPosition: function(row, col) {
var me = this;
// We were passed {row: 1, column: 2, view: myView} or [2, 1]
if (arguments.length === 1) {
// A [column, row] array passed
if (row.length) {
col = row[0];
row = row[1];
}
else if (row.isCellContext) {
return me.setAll(row.view, row.rowIdx, row.colIdx, row.record, row.column);
}
// An object containing {row: r, column: c}
else {
if (row.view) {
me.view = row.view;
}
col = row.column;
row = row.row;
}
}
me.setRow(row);
me.setColumn(col);
return me;
},
setAll: function(view, recordIndex, columnIndex, record, columnHeader) {
var me = this;
me.view = view;
me.rowIdx = recordIndex;
me.colIdx = columnIndex;
me.record = record;
me.column = columnHeader;
me.generation++;
return me;
},
setRow: function(row) {
var me = this,
dataSource = me.view.dataSource,
oldRecord = me.record,
count;
// eslint-disable-next-line eqeqeq
if (row != undefined) {
// Row index passed, < 0 meaning count from the tail (-1 is the last, etc)
if (typeof row === 'number') {
count = dataSource.getCount();
row = row < 0 ? Math.max(count + row, 0) : Math.max(Math.min(row, count - 1), 0);
me.rowIdx = row;
me.record = dataSource.getAt(row);
}
// row is a Record
else if (row.isModel) {
me.record = row;
me.rowIdx = dataSource.indexOf(row);
// expand all collapsed groups the record belongs to
// eslint-disable-next-line max-len
if (me.rowIdx === -1 && dataSource.isMultigroupStore && dataSource.isInCollapsedGroup(row)) {
dataSource.expandToRecord(row);
me.rowIdx = dataSource.indexOf(row);
}
}
// row is a grid row, or Element wrapping row
else if (row.tagName || row.isElement) {
me.record = me.view.getRecord(row);
// If it's a placeholder record for a collapsed group, index it correctly
// eslint-disable-next-line max-len
me.rowIdx = me.record ? (me.record.isCollapsedPlaceholder ? dataSource.indexOfPlaceholder(me.record) : dataSource.indexOf(me.record)) : -1;
}
}
if (me.record !== oldRecord) {
me.generation++;
}
return me;
},
setColumn: function(col) {
var me = this,
colMgr = me.view.getVisibleColumnManager(),
oldColumn = me.column;
// Maintainer:
// We MUST NOT update the context view with the column's view because this context
// may be for an Ext.locking.View which spans two grid views, and a column references
// its local grid view.
// eslint-disable-next-line eqeqeq
if (col != undefined) {
if (typeof col === 'number') {
me.colIdx = col;
me.column = colMgr.getHeaderAtIndex(col);
}
else if (col.isHeader) {
me.column = col;
// Must use the Manager's indexOf because view may be a locking view
// And Column#getVisibleIndex returns the index of the column within its own header.
me.colIdx = colMgr.indexOf(col);
}
}
if (me.column !== oldColumn) {
me.generation++;
}
return me;
},
setView: function(view) {
this.view = view;
this.refresh();
},
/**
* Returns the cell object referenced *at the time of calling*. Note that grid DOM is transient,
* and the cell referenced may be removed from the DOM due to paging or buffered rendering
* or column or record removal.
*
* @param {Boolean} returnDom Pass `true` to return a DOM object instead of an
* {@link Ext.dom.Element Element}.
* @return {HTMLElement/Ext.dom.Element} The cell referenced by this context.
*/
getCell: function(returnDom) {
return this.view.getCellByPosition(this, returnDom);
},
/**
* Returns the row object referenced *at the time of calling*. Note that grid DOM is transient,
* and the row referenced may be removed from the DOM due to paging or buffered rendering
* or column or record removal.
*
* @param {Boolean} returnDom Pass `true` to return a DOM object instead of an
* {@link Ext.dom.Element Element}.
* @return {HTMLElement/Ext.dom.Element} The grid row referenced by this context.
*/
getRow: function(returnDom) {
var result = this.view.getRow(this.record);
return returnDom ? result : Ext.get(result);
},
/**
* Returns the view node object (the encapsulating element of a data row) referenced
* *at the time of calling*. Note that grid DOM is transient, and the node referenced
* may be removed from the DOM due to paging or buffered rendering or column or record removal.
*
* @param {Boolean} returnDom Pass `true` to return a DOM object instead of an
* {@link Ext.dom.Element Element}.
* @return {HTMLElement/Ext.dom.Element} The grid item referenced by this context.
*/
getNode: function(returnDom) {
var result = this.view.getNode(this.record);
return returnDom ? result : Ext.get(result);
},
/**
* Compares this CellContext object to another CellContext to see if they refer to the same
* cell.
* @param {Ext.grid.CellContext} other The CellContext to compare.
* @return {Boolean} `true` if the other cell context references the same cell as this.
*/
isEqual: function(other) {
return other && other.isCellContext && other.record === this.record &&
other.column === this.column;
},
/**
* Creates a clone of this CellContext.
*
* The clone may be retargeted without affecting the reference of this context.
* @return {Ext.grid.CellContext} A copy of this context, referencing the same cell.
*/
clone: function() {
var me = this,
result = new me.self(me.view);
result.rowIdx = me.rowIdx;
result.colIdx = me.colIdx;
result.record = me.record;
result.column = me.column;
return result;
},
privates: {
isFirstColumn: function() {
var cell = this.getCell(true);
if (cell) {
return !cell.previousSibling;
}
},
isLastColumn: function() {
var cell = this.getCell(true);
if (cell) {
return !cell.nextSibling;
}
},
isLastRenderedRow: function() {
return this.view.all.endIndex === this.rowIdx;
},
getLastColumnIndex: function() {
var row = this.getRow(true);
if (row) {
return row.lastChild.cellIndex;
}
return -1;
},
refresh: function() {
var me = this,
newRowIdx = me.view.dataSource.indexOf(me.record),
newColIdx = me.view.getVisibleColumnManager().indexOf(me.column);
me.setRow(newRowIdx === -1 ? me.rowIdx : me.record);
me.setColumn(newColIdx === -1 ? me.colIdx : me.column);
},
/**
* @private
* Navigates left or right within the current row.
* @param {Number} direction `-1` to go towards the row start or `1` to go towards row end
*/
navigate: function(direction) {
var me = this,
columns = me.view.getVisibleColumnManager().getColumns();
switch (direction) {
case -1:
do {
// If we iterate off the start, wrap back to the end.
if (!me.colIdx) {
me.colIdx = columns.length - 1;
}
else {
me.colIdx--;
}
me.setColumn(me.colIdx);
} while (!me.getCell(true));
break;
case 1:
do {
// If we iterate off the end, wrap back to the start.
if (me.colIdx >= columns.length) {
me.colIdx = 0;
}
else {
me.colIdx++;
}
me.setColumn(me.colIdx);
} while (!me.getCell(true));
break;
}
}
},
statics: {
compare: function(c1, c2) {
return c1.rowIdx - c2.rowIdx || c1.colIdx - c2.colIdx;
}
}
});