/** * A class which encapsulates a range of cells defining a selection in a grid. * * Note that when range start and end points are represented by an array, the * order is traditional `x, y` order, that is column index followed by row index. * */Ext.define('Ext.grid.selection.Cells', { extend: 'Ext.dataview.selection.Selection', alias: 'selection.cells', requires: [ 'Ext.grid.Location' ], /** * @property {Boolean} isCells * This property indicates the this selection represents selected cells. * @readonly */ isCells: true, //------------------------------------------------------------------------- // Base Selection API clone: function() { var me = this, result = new me.self(me.view); if (me.startCell) { result.startCell = me.startCell.clone(); result.endCell = me.endCell.clone(); } return result; }, /** * Returns `true` if the passed {@link Ext.grid.Location cell context} is selected. * @param {Ext.grid.Location} cellLocation The cell location to test. * @return {Boolean} `true` if the passed {@link Ext.grid.Location cell context} is selected. */ isSelected: function(cellLocation) { var range, recordIndex, columnIndex; if (!cellLocation || !cellLocation.isGridLocation) { return false; } if (this.startCell) { // get start and end rows in the range range = this.getRowRange(); recordIndex = cellLocation.recordIndex; columnIndex = cellLocation.columnIndex; if (recordIndex >= range[0] && recordIndex <= range[1]) { // get start and end columns in the range range = this.getColumnRange(); return (columnIndex >= range[0] && columnIndex <= range[1]); } } return false; }, eachRow: function(fn, scope) { var me = this, rowRange = me.getRowRange(), store = me.view.store, rowIdx; for (rowIdx = rowRange[0]; rowIdx <= rowRange[1]; rowIdx++) { if (fn.call(scope || me, store.getAt(rowIdx)) === false) { return; } } }, eachColumn: function(fn, scope) { var colRange = this.getColumnRange(), columns = this.view.getVisibleColumns(), i; for (i = colRange[0]; i <= colRange[1]; i++) { if (fn.call(scope || this, columns[i], i) === false) { return; } } }, eachCell: function(fn, scope) { var me = this, view = me.view, store = view.store, rowRange = me.getRowRange(), colRange = me.getColumnRange(), baseLocation, location, rowIdx, colIdx; for (rowIdx = rowRange[0]; rowIdx <= rowRange[1]; rowIdx++) { baseLocation = new Ext.grid.Location(view, store.getAt(rowIdx)); for (colIdx = colRange[0]; colIdx <= colRange[1]; colIdx++) { location = baseLocation.cloneForColumn(colIdx); if (fn.call(scope || me, location, colIdx, rowIdx) === false) { return; } } } }, /** * @return {Number} The row index of the first row in the range or zero if no range. */ getFirstRowIndex: function() { return this.startCell ? Math.min(this.startCell.recordIndex, this.endCell.recordIndex) : 0; }, /** * @return {Number} The row index of the last row in the range or -1 if no range. */ getLastRowIndex: function() { return this.startCell ? Math.max(this.startCell.recordIndex, this.endCell.recordIndex) : -1; }, /** * @return {Number} The column index of the first column in the range or zero if no range. */ getFirstColumnIndex: function() { return this.startCell ? Math.min(this.startCell.columnIndex, this.endCell.columnIndex) : 0; }, /** * @return {Number} The column index of the last column in the range or -1 if no range. */ getLastColumnIndex: function() { return this.startCell ? Math.max(this.startCell.columnIndex, this.endCell.columnIndex) : -1; }, //------------------------------------------------------------------------- privates: { /** * @private */ clear: function(suppressEvent) { var me = this, view = me.view, changed; if (view.getVisibleColumns().length) { me.eachCell(function(location) { view.onCellDeselect(location); changed = true; }); } me.startCell = me.endCell = null; if (changed && !suppressEvent) { this.getSelectionModel().fireSelectionChange(); } }, /** * Used during drag/shift+downarrow range selection on start. * @param {Ext.grid.Location} startCell The start cell of the cell drag selection. * @private */ setRangeStart: function (startCell) { // Must clone them. Users might use one instance and reconfigure it to navigate. this.startCell = (this.endCell = startCell.clone()).clone(); this.view.onCellSelect(startCell); }, /** * Used during drag/shift+downarrow range selection on drag. * @param {Ext.grid.Location} endCell The end cell of the cell drag selection. * @private */ setRangeEnd: function (endCell) { var me = this, view = me.view, store = view.store, renderInfo = view.renderInfo, maxColIdx = view.getVisibleColumns().length - 1, range, lastRange, rowStart, rowEnd, colStart, colEnd, rowIdx, colIdx, location, baseLocation; me.endCell = endCell.clone(); range = me.getRange(); lastRange = me.lastRange || range; rowStart = Math.max(Math.min(range[0][1], lastRange[0][1]), renderInfo.indexTop); rowEnd = Math.min(Math.max(range[1][1], lastRange[1][1]), renderInfo.indexBottom - 1); colStart = Math.min(range[0][0], lastRange[0][0]); colEnd = Math.min(Math.max(range[1][0], lastRange[1][0]), maxColIdx); // Loop through the union of last range and current range for (rowIdx = rowStart; rowIdx <= rowEnd; rowIdx++) { baseLocation = new Ext.grid.Location(view, store.getAt(rowIdx)); for (colIdx = colStart; colIdx <= colEnd; colIdx++) { location = baseLocation.cloneForColumn(colIdx); // If we are outside the current range, deselect if (rowIdx < range[0][1] || rowIdx > range[1][1] || colIdx < range[0][0] || colIdx > range[1][0]) { view.onCellDeselect(location); } else { view.onCellSelect(location); } } } me.lastRange = range; }, extendRange: function(extensionVector) { var me = this, view = me.view, newEndCell; if (extensionVector[extensionVector.type] < 0) { newEndCell = new Ext.grid.Location(view, { record: me.getLastRowIndex(), column: me.getLastColumnIndex() }); me.startCell = extensionVector.start.clone(); me.setRangeEnd(newEndCell); me.view.getNavigationModel().setPosition(extensionVector.start); } else { me.startCell = new Ext.grid.Location(view, { record: me.getFirstRowIndex(), column: me.getFirstColumnIndex() }); me.setRangeEnd(extensionVector.end); me.view.getNavigationModel().setLocation(extensionVector.end); } }, /** * Returns the `[[x, y],[x,y]]` coordinates in top-left to bottom-right order * of the current selection. * * If no selection, returns [[0, 0],[-1, -1]] so that an incrementing iteration * will not execute. * * @return {Number[][]} * @private */ getRange: function() { return [[this.getFirstColumnIndex(), this.getFirstRowIndex()], [this.getLastColumnIndex(), this.getLastRowIndex()]]; }, /** * Returns the size of the selection rectangle. * @return {Number} * @private */ getRangeSize: function() { return this.getCount(); }, /** * @private * Used by the SelectionModel to fire the selectionchange event with the batch of selected records */ getRecords: function() { var rowRange = this.getRowRange(); return this.getSelectionModel().getStore().getRange(rowRange[0], rowRange[1]); }, /** * Returns the number of cells selected. * @return {Number} The nuimber of cells selected * @private */ getCount: function() { var range = this.getRange(); return (range[1][0] - range[0][0] + 1) * (range[1][1] - range[0][1] + 1); }, /** * @private */ selectAll: function() { var me = this, view = me.view, columns = view.getVisibleColumns(); me.clear(); me.setRangeStart(new Ext.grid.Location(view, {record: 0, column: 0})); me.setRangeEnd(new Ext.grid.Location(view, {record: view.store.last(), column: columns[columns.length - 1]})); }, /** * @return {Boolean} * @private */ isAllSelected: function() { var start = this.rangeStart, end = this.rangeEnd; // All selected only if we encompass the entire store and every visible column if (start) { if (!start.columnIndex && !start.recordIndex) { return end.columnIndex === end.view.getVisibleColumns().length - 1 && end.recordIndex === end.view.store.getCount - 1; } } return false; }, /** * @return {Number[]} The column range which encapsulates the range. * @private */ getColumnRange: function() { return [this.getFirstColumnIndex(), this.getLastColumnIndex()]; }, /** * @private * Called through {@link Ext.grid.selection.SpreadsheetModel#getLastSelected} by {@link Ext.panel.Table#updateBindSelection} when publishing the `selection` property. * It should yield the last record selected. */ getLastSelected: function() { return this.view.getStore().getAt(this.endCell.recordIndex); }, /** * Returns the row range which encapsulates the range - the view range that needs * updating. * @return {Number[]} * @private */ getRowRange: function() { return [this.getFirstRowIndex(), this.getLastRowIndex()]; }, onSelectionFinish: function() { var me = this, view = me.view; if (me.getCount()) { me.getSelectionModel().onSelectionFinish(me, new Ext.grid.Location(view, {record: me.getFirstRowIndex(), column: me.getFirstColumnIndex()}), new Ext.grid.Location(view, {record: me.getLastRowIndex(), column: me.getLastColumnIndex()})); } else { me.getSelectionModel().onSelectionFinish(me); } } }});