/**
* A selection model for {@link Ext.grid.Panel grids} which allows you to select data in
* a spreadsheet-like manner.
*
* Supported features:
*
* - Single / Range / Multiple individual row selection.
* - Single / Range cell selection.
* - Column selection by click selecting column headers.
* - Select / deselect all by clicking in the top-left, header.
* - Adds row number column to enable row selection.
* - Optionally you can enable row selection using checkboxes
*
* # Example usage
*
* @example
* var store = Ext.create('Ext.data.Store', {
* fields: ['name', 'email', 'phone'],
* data: [
* { name: 'Lisa', email: 'lisa@simpsons.com', phone: '555-111-1224' },
* { name: 'Bart', email: 'bart@simpsons.com', phone: '555-222-1234' },
* { name: 'Homer', email: 'homer@simpsons.com', phone: '555-222-1244' },
* { name: 'Marge', email: 'marge@simpsons.com', phone: '555-222-1254' }
* ]
* });
*
* Ext.create('Ext.grid.Panel', {
* title: 'Simpsons',
* store: store,
* width: 400,
* renderTo: Ext.getBody(),
* columns: [
* { text: 'Name', dataIndex: 'name' },
* { text: 'Email', dataIndex: 'email', flex: 1 },
* { text: 'Phone', dataIndex: 'phone' }
* ],
* selModel: {
* type: 'spreadsheet'
* }
* });
*
* # Using {@link Ext.data.BufferedStore}s
* It is very important to remember that a {@link Ext.data.BufferedStore} does *not* contain the
* full dataset. The purpose of a BufferedStore is to only hold in the client, a range of
* pages from the dataset that corresponds with what is currently visible in the grid
* (plus a few pages above and below the visible range to allow fast scrolling).
*
* When using "select all" rows and a BufferedStore, an `allSelected` flag is set, and so all
* records which are read into the client side cache will thenceforth be selected, and will
* be rendered as selected in the grid.
*
* *But records which have not been read into the cache will obviously not be available
* when interrogating selected records. As you scroll through the dataset, and more
* pages are read from the server, they will become available to add to the selection.*
*
* @since 5.1.0
*/
Ext.define('Ext.grid.selection.SpreadsheetModel', {
extend: 'Ext.selection.Model',
requires: [
'Ext.grid.selection.Selection',
'Ext.grid.selection.Cells',
'Ext.grid.selection.Rows',
'Ext.grid.selection.Columns',
'Ext.grid.selection.SelectionExtender' // TODO: cmd-auto-dependency
],
alias: 'selection.spreadsheet',
isSpreadsheetModel: true,
config: {
/**
* @cfg {Boolean} [columnSelect=false]
* Set to `true` to enable selection of columns.
*
* **NOTE**: This will remove sorting on header click and instead provide column
* selection and deselection. Sorting is still available via column header menu.
*/
columnSelect: {
$value: false,
lazy: true
},
/**
* @cfg {Boolean} [cellSelect=true]
* Set to `true` to enable selection of individual cells or a single rectangular
* range of cells. This will provide cell range selection using click, and
* potentially drag to select a rectangular range. You can also use "SHIFT + arrow"
* key navigation to select a range of cells.
*/
cellSelect: {
$value: true,
lazy: true
},
/**
* @cfg {Boolean} [rowSelect=true]
* Set to `true` to enable selection of rows by clicking on a row number column.
*
* *Note*: This feature will add the row number as the first column.
*/
rowSelect: {
$value: true,
lazy: true
},
/**
* @cfg {Boolean} [dragSelect=true]
* Set to `true` to enables cell range selection by cell dragging.
*/
dragSelect: {
$value: true,
lazy: true
},
/**
* @cfg {Ext.grid.selection.Selection} [selected]
* Pass an instance of one of the subclasses of {@link Ext.grid.selection.Selection}.
*/
selected: null,
/**
* @cfg {String} extensible
* This configures whether this selection model is to implement a mouse based dragging
* gesture to extend a *contiguous* selection.
*
* Note that if there are multiple, discontiguous selected rows or columns, selection
* extension is not available.
*
* If set, then the bottom right corner of the contiguous selection will display
* a drag handle. By dragging this, an extension area may be defined into which
* the selection is extended.
*
* Upon the end of the drag, the
* {@link Ext.panel.Table#beforeselectionextend beforeselectionextend} event will be fired
* though the encapsulating grid. Event handlers may manipulate the store data in any way.
*
* Possible values for this configuration are
*
* - `"x"` Only allow extending the block to the left or right.
* - `"y"` Only allow extending the block above or below.
* - `"xy"` Allow extending the block in both dimensions.
* - `"both"` Allow extending the block in both dimensions.
* - `true` Allow extending the block in both dimensions.
* - `false` Disable the extensible feature
* - `null` Disable the extensible feature
*
* It's important to notice that setting this to `"both"`, `"xy"` or `true` will allow you
* to extend the selection in both directions, but only one direction at a time.
* It will NOT be possible to drag it diagonally.
*/
extensible: {
$value: true,
lazy: true
},
/**
* @cfg {Boolean} reducible
* @since 6.6.0
* This configures if the extensible config is also allowed to reduce its selection
*
* Note: This is only relevant if `extensible` is not `false` or `null`
*/
reducible: true
},
/**
* @event selectionchange
* Fired *by the grid* after the selection changes. Return `false` to veto the selection
* extension.
*
* Note that the behavior of selectionchange is different in Ext 6.x vs. Ext 5. In Ext 6.x,
* if rows are being selected, a block of records is passed as the second parameter.
* In Ext 5, the selection object was passed.
*
*
* @param {Ext.grid.Panel} grid The grid whose selection has changed.
* @param {Ext.grid.selection.Selection} selection A subclass of
* {@link Ext.grid.selection.Selection} describing the new selection.
*/
/**
* @cfg {Boolean} checkboxSelect [checkboxSelect=false]
* Enables selection of the row via clicking on checkbox. Note: this feature will add
* new column at position specified by {@link #checkboxColumnIndex}.
*/
checkboxSelect: false,
/**
* @cfg {Number/String} [checkboxColumnIndex=0]
* The index at which to insert the checkbox column.
* Supported values are a numeric index, and the strings 'first' and 'last'. Only valid when set
* *before* render.
*/
checkboxColumnIndex: 0,
/**
* @cfg {Boolean} [showHeaderCheckbox=true]
* Configure as `false` to not display the header checkbox at the top of the checkbox column
* when {@link #checkboxSelect} is set.
*/
showHeaderCheckbox: true,
/**
* @cfg {String} [checkColumnHeaderText]
* Displays the configured text in the check column's header.
*
* if {@link #cfg-showHeaderCheckbox} is `true`, the text is shown *above* the checkbox.
* @since 6.0.1
*/
checkColumnHeaderText: null,
/**
* @cfg {Number/String} [checkboxHeaderWidth=24]
* Width of checkbox column.
*/
checkboxHeaderWidth: 24,
/**
* @cfg {Number/String} [rowNumbererHeaderWidth=46]
* Width of row numbering column.
*/
rowNumbererHeaderWidth: 46,
columnSelectCls: Ext.baseCSSPrefix + 'ssm-column-select',
rowNumbererHeaderCls: Ext.baseCSSPrefix + 'ssm-row-numberer-hd',
tdCls: Ext.baseCSSPrefix + 'grid-cell-special ' + Ext.baseCSSPrefix + 'selmodel-column',
/**
* @method getCount
* This method is not supported by SpreadsheetModel.
*
* To interrogate the selection use {@link #cfg!selected}'s getter, which will return
* an instance of one of the three selection types, or `null` if no selection.
*
* The three selection types are:
*
* * {@link Ext.grid.selection.Rows}
* * {@link Ext.grid.selection.Columns}
* * {@link Ext.grid.selection.Cells}
*/
/**
* @method getSelectionMode
* This method is not supported by SpreadsheetModel.
*/
/**
* @method setSelectionMode
* This method is not supported by SpreadsheetModel.
*/
/**
* @method setLocked
* This method is not currently supported by SpreadsheetModel.
*/
/**
* @method isLocked
* This method is not currently supported by SpreadsheetModel.
*/
/**
* @method isRangeSelected
* This method is not supported by SpreadsheetModel.
*
* To interrogate the selection use {@link #cfg!selected}'s getter, which will return
* an instance of one of the three selection types, or `null` if no selection.
*
* The three selection types are:
*
* * {@link Ext.grid.selection.Rows}
* * {@link Ext.grid.selection.Columns}
* * {@link Ext.grid.selection.Cells}
*/
/**
* @member Ext.panel.Table
* @event beforeselectionextend An event fired when an extension block is extended
* using a drag gesture. Only fired when the SpreadsheetSelectionModel is used and
* configured with the
* {@link Ext.grid.selection.SpreadsheetModel#extensible extensible} config.
* @param {Ext.panel.Table} grid The owning grid.
* @param {Ext.grid.selection.Selection} An object which encapsulates a contiguous
* selection block.
* @param {Object} extension An object describing the type and size of extension.
* @param {String} extension.type `"rows"` or `"columns"`
* @param {Ext.grid.CellContext} extension.start The start (top left) cell of the
* extension area.
* @param {Ext.grid.CellContext} extension.end The end (bottom right) cell of the
* extension area.
* @param {number} [extension.columns] The number of columns extended (-ve means
* on the left side).
* @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
*/
/**
* @member Ext.panel.Table
* @event selectionextenderdrag An event fired when an extension block is dragged to
* encompass a new range. Only fired when the SpreadsheetSelectionModel is used and
* configured with the
* {@link Ext.grid.selection.SpreadsheetModel#extensible extensible} config.
* @param {Ext.panel.Table} grid The owning grid.
* @param {Ext.grid.selection.Selection} An object which encapsulates a contiguous
* selection block.
* @param {Object} extension An object describing the type and size of extension.
* @param {String} extension.type `"rows"` or `"columns"`
* @param {HTMLElement} extension.overCell The grid cell over which the mouse is being dragged.
* @param {Ext.grid.CellContext} extension.start The start (top left) cell of the
* extension area.
* @param {Ext.grid.CellContext} extension.end The end (bottom right) cell of the
* extension area.
* @param {number} [extension.columns] The number of columns extended (-ve means
* on the left side).
* @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
*/
/**
* @private
*/
bindComponent: function(view) {
var me = this,
viewListeners,
storeListeners,
lockedGrid;
if (me.view !== view) {
if (me.view) {
me.navigationModel = null;
Ext.destroy(me.viewListeners, me.navigationListeners);
}
me.view = view;
if (view) {
// We need to realize our lazy configs now that we have the view...
me.getCellSelect();
lockedGrid = view.ownerGrid.lockedGrid;
// If there is a locked grid, process it now
if (lockedGrid) {
me.hasLockedHeader = true;
me.onViewCreated(lockedGrid, lockedGrid.getView());
}
// Otherwise, get back to us when the view is fully created
// so that we can tweak its headerCt
else {
view.grid.on({
viewcreated: me.onViewCreated,
scope: me,
single: true
});
}
me.gridListeners = view.ownerGrid.on({
columnschanged: me.onColumnsChanged,
columnmove: me.onColumnMove,
scope: me,
destroyable: true
});
storeListeners = me.getStoreListeners();
storeListeners.scope = me;
storeListeners.destroyable = true;
me.storeListeners = me.store.on(storeListeners);
viewListeners = me.getViewListeners();
viewListeners.scope = me;
viewListeners.destroyable = true;
me.viewListeners = view.on(viewListeners);
me.navigationModel = view.getNavigationModel();
me.navigationListeners = me.navigationModel.on({
navigate: me.onNavigate,
scope: me,
destroyable: true
});
// Add class to add special cursor pointer to column headers
if (me.getColumnSelect()) {
view.ownerGrid.addCls(me.columnSelectCls);
}
me.updateHeaderState();
}
}
},
/**
* Retrieve a configuration to be used in a HeaderContainer.
* This should be used when checkboxSelect is set to false.
* @protected
*/
getCheckboxHeaderConfig: function() {
var me = this,
showCheck = me.showHeaderCheckbox !== false;
return {
xtype: 'checkcolumn',
// historically used as a discriminator property before isCheckColumn
isCheckerHd: showCheck,
headerCheckbox: showCheck,
ignoreExport: true,
text: me.checkColumnHeaderText,
clickTargetName: 'el',
width: me.checkboxHeaderWidth,
sortable: false,
draggable: false,
resizable: false,
hideable: false,
menuDisabled: true,
tdCls: me.tdCls,
cls: Ext.baseCSSPrefix + 'selmodel-column',
stopSelection: false,
editRenderer: me.editRenderer || me.renderEmpty,
locked: me.hasLockedHeader,
updateHeaderState: me.updateHeaderState.bind(me),
// It must not attempt to set anything in the records on toggle.
// We handle that in onHeaderClick.
toggleAll: Ext.emptyFn,
// The selection model listens to the navigation model to select/deselect
setRecordCheck: Ext.emptyFn,
// It uses our isRowSelected to test whether a row is checked
isRecordChecked: Ext.emptyFn
};
},
renderEmpty: function() {
return '\u00a0';
},
/**
* @private
*/
getStoreListeners: function() {
var me = this,
r = me.callParent();
r.priority = 2000;
r.refresh = me.onStoreChanged;
r.clear = me.onStoreChanged;
return r;
},
/**
* @private
*/
onHeaderClick: function(headerCt, header, e) {
// Template method. See base class
var me = this,
sel = me.selected,
isSelected = false;
if (header === me.numbererColumn || header === me.checkColumn) {
e.stopEvent();
// Not all selected, select all
if (!sel || !sel.isAllSelected()) {
me.selectAll();
}
else {
me.deselectAll();
}
me.updateHeaderState();
me.lastColumnSelected = null;
}
else if (me.columnSelect) {
if (e.shiftKey && sel && sel.lastColumnSelected) {
sel.setRangeEnd(header);
me.fireSelectionChange();
}
else {
// keeping track of the column selection status before we go through the clear block
isSelected = me.isColumnSelected(header);
if (sel) {
if (!e.ctrlKey) {
sel.clear();
me.updateSelectionExtender();
}
else if (isSelected) {
me.deselectColumn(header);
me.selected.lastColumnSelected = null;
}
}
if (!isSelected || (!e.ctrlKey && e.pointerType !== 'touch')) {
me.selectColumn(header, e.ctrlKey);
sel = me.selected;
sel.lastColumnSelected = header;
if (!sel.startColumn) {
sel.startColumn = header;
}
}
}
me.lastOverColumn = header;
}
},
selectByPosition: function(position) {
var me = this;
position = new Ext.grid.CellContext(me.view).setPosition(position.row, position.column);
if (me.getCellSelect()) {
me.selectCells(position, position);
}
else if (me.getRowSelect()) {
this.select(position.record);
}
else if (me.getColumnSelect()) {
me.selectColumn(position.column);
}
},
/**
* @private
*/
updateHeaderState: function() {
// check to see if all records are selected
var me = this,
store = me.view.dataSource,
views = me.views,
sel = me.selected,
isChecked = false,
checkHd = me.checkColumn,
storeCount;
if (store && sel && sel.isRows) {
storeCount = store.getCount();
if (store.isBufferedStore) {
isChecked = sel.allSelected;
}
else {
isChecked = storeCount > 0 && (storeCount === sel.getCount());
}
}
if (views && views.length) {
if (checkHd) {
checkHd.setHeaderStatus(isChecked);
}
}
},
onBindStore: function(store, oldStore, initial) {
if (!initial) {
this.onStoreRefresh();
}
},
/**
* Handles the grid's beforereconfigure event.
* Adds the checkbox header if the columns have been reconfigured.
* Also adds the row numberer.
* @param {Ext.panel.Table} grid
* @param {Ext.data.Store} store
* @param {Object[]} columns
* @param {Ext.data.Store} oldStore
* @param {Object[]} oldColumns
* @private
*/
onBeforeReconfigure: function(grid, store, columns, oldStore, oldColumns) {
var me = this,
checkboxColumnIndex = me.checkboxColumnIndex;
if (columns) {
Ext.suspendLayouts();
if (me.numbererColumn) {
me.numbererColumn.ownerCt.remove(me.numbererColumn, false);
columns.unshift(me.numbererColumn);
}
if (me.checkColumn) {
if (checkboxColumnIndex === 'first') {
checkboxColumnIndex = 0;
}
else if (checkboxColumnIndex === 'last') {
checkboxColumnIndex = columns.length;
}
me.checkColumn.ownerCt.remove(me.checkColumn, false);
Ext.Array.insert(columns, checkboxColumnIndex, [me.checkColumn]);
}
Ext.resumeLayouts();
}
},
/**
* This is a helper method to create a cell context which encapsulates one cell in a grid view.
*
* It will contain the following properties:
* colIdx - column index
* rowIdx - row index
* column - {@link Ext.grid.column.Column Column} under which the cell is located.
* record - {@link Ext.data.Model} Record from which the cell derives its data.
* view - The view. If this selection model is for a locking grid, this will be the
* outermost view, the {@link Ext.grid.locking.View} which encapsulates the sub
* grids. Column indices are relative to the outermost view's visible column set.
*
* @param {Number} record Record for which to select the cell, or row index.
* @param {Number} column Grid column header, or column index.
* @return {Ext.grid.CellContext} A context object describing the cell. Note that the
* `rowidx` and `colIdx` properties are only valid
* at the time the context object is created. Column movement, sorting or filtering
* might changed where the cell is.
* @private
*/
getCellContext: function(record, column) {
return new Ext.grid.CellContext(this.view.ownerGrid.getView()).setPosition(record, column);
},
select: function(records, keepExisting, suppressEvent) {
// API docs are inherited
var me = this,
sel = me.selected,
view = me.view,
store = view.dataSource,
len,
i,
record,
changed = false;
// Ensure selection object is of the correct type
if (!sel || !sel.isRows) {
me.resetSelection(true);
sel = me.selected = new Ext.grid.selection.Rows(view);
}
else if (!keepExisting) {
sel.clear();
}
if (!Ext.isArray(records)) {
records = [records];
}
len = records.length;
for (i = 0; i < len; i++) {
record = records[i];
if (typeof record === 'number') {
record = store.getAt(record);
}
if (!sel.contains(record)) {
sel.add(record);
changed = true;
}
}
if (changed) {
me.updateHeaderState();
if (!suppressEvent) {
me.fireSelectionChange();
}
}
},
deselect: function(records, suppressEvent) {
// API docs are inherited
var me = this,
sel = me.selected,
store = me.view.dataSource,
len,
i,
record,
changed = false;
if (sel && sel.isRows) {
if (!Ext.isArray(records)) {
records = [records];
}
len = records.length;
for (i = 0; i < len; i++) {
record = records[i];
if (typeof record === 'number') {
record = store.getAt(record);
}
sel.remove(record);
if (!changed) {
changed = true;
}
}
}
if (changed) {
me.updateHeaderState();
if (!suppressEvent) {
me.fireSelectionChange();
}
}
},
/* eslint-disable max-len */
/**
* This method allows programmatic selection of the cell range.
*
* @example
* var store = Ext.create('Ext.data.Store', {
* fields : ['name', 'email', 'phone'],
* data : {
* items : [
* { name : 'Lisa', email : 'lisa@simpsons.com', phone : '555-111-1224' },
* { name : 'Bart', email : 'bart@simpsons.com', phone : '555-222-1234' },
* { name : 'Homer', email : 'homer@simpsons.com', phone : '555-222-1244' },
* { name : 'Marge', email : 'marge@simpsons.com', phone : '555-222-1254' }
* ]
* },
* proxy : {
* type : 'memory',
* reader : {
* type : 'json',
* root : 'items'
* }
* }
* });
*
* var grid = Ext.create('Ext.grid.Panel', {
* title : 'Simpsons',
* store : store,
* width : 400,
* renderTo : Ext.getBody(),
* columns : [
* columns: [
* { text: 'Name', dataIndex: 'name' },
* { text: 'Email', dataIndex: 'email', flex: 1 },
* { text: 'Phone', dataIndex: 'phone', width:120 },
* {
* text:'Combined', dataIndex: 'name', width : 300,
* renderer: function(value, metaData, record, rowIndex, colIndex, store, view) {
* console.log(arguments);
* return value + ' has email: ' + record.get('email');
* }
* }
* ],
* ],
* selType: 'spreadsheet'
* });
*
* var model = grid.getSelectionModel(); // get selection model
*
* // We will create range of 4 cells.
*
* // Now set the range and prevent rangeselect event from being fired.
* // We can use a simple array when we have no locked columns.
* model.selectCells([0, 0], [1, 1], true);
*
* @param rangeStart {Ext.grid.CellContext/Number[]} Range starting position. Can be either Cell
* context or a `[rowIndex, columnIndex]` numeric array.
*
* Note that when a numeric array is used in a locking grid, the column indices are relative
* to the outermost grid, encompassing locked *and* normal sides.
* @param rangeEnd {Ext.grid.CellContext/Number[]} Range end position. Can be either
* Cell context or a `[rowIndex, columnIndex]` numeric array.
*
* Note that when a numeric array is used in a locking grid, the column indices are relative
* to the outermost grid, encompassing locked *and* normal sides.
* @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
* `{@link #selectionchange}` event.
*/
selectCells: function(rangeStart, rangeEnd, suppressEvent) {
var me = this,
view = me.view.ownerGrid.view,
sel;
rangeStart = rangeStart.isCellContext
? rangeStart.clone()
: new Ext.grid.CellContext(view).setPosition(rangeStart);
rangeEnd = rangeEnd.isCellContext
? rangeEnd.clone()
: new Ext.grid.CellContext(view).setPosition(rangeEnd);
me.resetSelection(true);
me.selected = sel = new Ext.grid.selection.Cells(rangeStart.view);
sel.setRangeStart(rangeStart);
sel.setRangeEnd(rangeEnd);
if (!suppressEvent) {
me.fireSelectionChange();
}
},
/* eslint-enable max-len */
/**
* Select all the data if possible.
*
* If {@link #rowSelect} is `true`, then all *records* will be selected.
*
* If {@link #cellSelect} is `true`, then all *rendered cells* will be selected.
*
* If {@link #columnSelect} is `true`, then all *columns* will be selected.
*
* @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
* `{@link #selectionchange}` event.
*/
selectAll: function(suppressEvent) {
var me = this,
sel = me.selected,
doSelect,
view = me.view;
if (me.rowSelect) {
if (!sel || !sel.isRows) {
me.resetSelection(true);
me.selected = sel = new Ext.grid.selection.Rows(view);
}
doSelect = true;
}
else if (me.cellSelect) {
if (!sel || !sel.isCells) {
me.resetSelection(true);
me.selected = sel = new Ext.grid.selection.Cells(view);
}
doSelect = true;
}
else if (me.columnSelect) {
if (!sel || !sel.isColumns) {
me.resetSelection(true);
me.selected = sel = new Ext.grid.selection.Columns(view);
}
doSelect = true;
}
if (sel) {
sel.allSelected = true;
}
if (doSelect) {
me.updateHeaderState();
sel.selectAll(); // this populates the selection with the records
if (!suppressEvent) {
me.fireSelectionChange();
}
}
},
/**
* Clears the selection.
* @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
* `{@link #selectionchange}` event.
*/
deselectAll: function(suppressEvent) {
var me = this,
sel = me.selected;
if (sel && sel.getCount()) {
sel.clear();
sel.allSelected = false;
me.updateHeaderState();
if (!suppressEvent) {
me.fireSelectionChange();
}
}
},
/**
* Select one or more rows.
* @param rows {Ext.data.Model[]} Records to select.
* @param {Boolean} [keepSelection=false] Pass `true` to keep previous selection.
* @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
* `{@link #selectionchange}` event.
*/
selectRows: function(rows, keepSelection, suppressEvent) {
var me = this,
sel = me.selected,
isSelectingRows = sel && sel.isRows,
len = rows.length,
i;
if (!keepSelection || !isSelectingRows) {
me.resetSelection(true);
}
if (!isSelectingRows) {
me.selected = sel = new Ext.grid.selection.Rows(me.view);
}
if (rows.isEntity) {
sel.add(rows);
}
else {
for (i = 0; i < len; i++) {
sel.add(rows[i]);
}
}
if (!suppressEvent) {
me.fireSelectionChange();
}
},
isSelected: function(record) {
// API docs are inherited.
return this.isRowSelected(record);
},
/**
* Selects a column.
* @param {Ext.grid.column.Column} column Column to select.
* @param {Boolean} [keepSelection=false] Pass `true` to keep previous selection.
* @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
* `{@link #selectionchange}` event.
*/
selectColumn: function(column, keepSelection, suppressEvent) {
var me = this,
selData = me.selected,
view = column.getView();
// Clear other selection types
if (!selData || !selData.isColumns || selData.view !== view.ownerGrid.view) {
me.resetSelection(true);
me.selected = selData = new Ext.grid.selection.Columns(view);
}
if (!selData.contains(column)) {
if (!keepSelection) {
selData.clear();
}
selData.add(column);
me.updateHeaderState();
if (!suppressEvent) {
me.fireSelectionChange();
}
}
},
/**
* Deselects a column.
* @param {Ext.grid.column.Column} column Column to deselect.
* @param {Boolean} [suppressEvent] Pass `true` to prevent firing the
* `{@link #selectionchange}` event.
*/
deselectColumn: function(column, suppressEvent) {
var me = this,
selData = me.getSelected();
if (selData && selData.isColumns && selData.contains(column)) {
selData.remove(column);
me.updateHeaderState();
if (!suppressEvent) {
me.fireSelectionChange();
}
}
},
getSelection: function() {
// API docs are inherited.
// Superclass returns array of selected records
var selData = this.selected;
if (selData && selData.isRows) {
return selData.getRecords();
}
return [];
},
destroy: function() {
var me = this,
scrollEls = me.scrollEls;
Ext.destroy(me.gridListeners, me.viewListeners, me.selected,
me.navigationListeners, me.extensible);
if (scrollEls) {
Ext.dd.ScrollManager.unregister(scrollEls);
}
if (me._onMouseUp && !me._onMouseUp.destroyed) {
me.stopAutoScroller();
me._onMouseUp.destroy();
}
me.selected = me.gridListeners = me.viewListeners = me.selectionData =
me.navigationListeners = me.scrollEls = null;
me.callParent();
},
//-------------------------------------------------------------------------
privates: {
/**
* @property {Object} axesConfigs
* Use when converting the extensible config into a SelectionExtender
* to create its `axes` config to specify which axes it may extend.
* @private
*/
axesConfigs: {
x: 1,
y: 2,
xy: 3,
both: 3,
"true": 3 // reserved word MUST be quoted when used an a property name
},
getNumbererColumnConfig: function() {
var me = this;
return {
xtype: 'rownumberer',
width: me.rowNumbererHeaderWidth,
editRenderer: me.renderEmpty,
tdCls: me.rowNumbererTdCls,
cls: me.rowNumbererHeaderCls,
locked: me.hasLockedHeader
};
},
/**
* @return {Object}
* @private
*/
getViewListeners: function() {
return {
refresh: this.onViewRefresh,
keyup: {
element: 'el',
fn: this.onViewKeyUp,
scope: this
}
};
},
/**
* @private
*/
onViewKeyUp: function(e) {
var sel = this.selected;
// Released the shift key, terminate a keyboard based range selection
if (e.keyCode === e.SHIFT && sel && sel.isRows && sel.getRangeSize()) {
// Copy the drag range into the selected records collection
sel.addRange();
}
},
/**
* @private
*/
onStoreChanged: function() {
var me = this,
selData = me.selected;
if (selData) {
if (selData.isCells) {
me.resetSelection();
}
else if (selData.isRows) {
if (me.pruneRemoved === false && selData.selectedRecords.length) {
me.refresh();
}
else {
me.resetSelection();
}
}
}
},
/**
* @private
*/
onColumnsChanged: function() {
var me = this,
selectionChanged = me.onViewChanged(me.view, true);
// This event is fired directly from the HeaderContainer before the view updates.
// So we have to wait until idle to update the selection UI.
// NB: fireSelectionChange calls updateSelectionExtender after firing its event.
Ext.on('idle', selectionChanged ? me.fireSelectionChange : me.updateSelectionExtender,
me, { single: true });
},
// The selection may have acquired or lost contiguity, so the replicator may need
// enabling or disabling
onColumnMove: function() {
this.updateSelectionExtender();
},
/**
* @private
*/
onViewRefresh: function(view) {
var me = this,
selectionChanged = me.onViewChanged(view);
// The selection may have acquired or lost contiguity, so the replicator may need
// enabling or disabling
// NB: fireSelectionChange calls updateSelectionExtender after firing its event.
me[selectionChanged ? 'fireSelectionChange' : 'updateSelectionExtender']();
},
/**
* @private
*/
resetSelection: function(suppressEvent) {
var sel = this.selected;
if (sel) {
sel.clear();
if (!suppressEvent) {
this.fireSelectionChange();
}
}
},
/**
* When the view has changed, whether it be to a refresh or a column change, we need
* to check the current selection and deselect anything that may no longer be valid.
* @param {Ext.view.Table} view
* @param {Boolean} isColumnChange `true` if this change is based on a column change
* @returns {Boolean} `true` if a change to the selection was made
* @private
* @since 6.2.2
*/
onViewChanged: function(view, isColumnChange) {
var me = this,
selData = me.selected,
store = view.store,
selectionChanged = false,
rowRange, colCount, colIdx, rowIdx, context;
// When columns have changed, we have to deselect *every* cell in the row range
// because we do not know where the columns have gone to.
if (selData) {
view = selData.view;
if (isColumnChange) {
if (selData.isCells) {
context = new Ext.grid.CellContext(view);
rowRange = selData.getRowRange();
colCount = view.ownerGrid.getColumnManager().getColumns().length;
if (colCount) {
for (rowIdx = rowRange[0]; rowIdx <= rowRange[1]; rowIdx++) {
context.setRow(rowIdx);
for (colIdx = 0; colIdx < colCount; colIdx++) {
// CellContext only works with visible columns and this index is
// potentially a hidden column. Ensure the column is available
// before deselecting the cell.
context.setColumn(colIdx);
if (context.column) {
view.onCellDeselect(context);
}
// Selection may still reference a hidden column and may need
// to be cleared
if (me.maybeClearSelection(context)) {
selectionChanged = true;
}
}
}
}
else {
me.clearSelections();
selectionChanged = true;
}
}
// We have to deselect columns which have been hidden/removed
else if (selData.isColumns) {
selectionChanged = false;
selData.eachColumn(function(column, columnIdx) {
if (!column.isVisible() || !view.ownerGrid.isAncestor(column)) {
me.remove(column);
if (me.maybeClearSelection({ column: column })) {
selectionChanged = true;
}
}
});
}
}
// View has refreshed; deselect filtered out records
else if (selData.isRows && store.isFiltered()) {
selData.eachRow(function(rec) {
if (!store.contains(rec)) {
// Maintainer: `this` is the Rows selection object, *NOT* me.
this.remove(rec);
if (me.maybeClearSelection({ rowIdx: view.indexOf(rec) })) {
selectionChanged = true;
}
}
});
}
}
return selectionChanged;
},
onViewCreated: function(grid, view) {
var me = this,
ownerGrid = view.ownerGrid,
headerCt = view.headerCt;
// Only add columns to the locked view, or only view if there is no twin
if (!ownerGrid.lockable || view.isLockedView) {
// if there is no row number column and we ask for it, then it should be added here
if (me.getRowSelect()) {
// Ensure we have a rownumber column
me.getNumbererColumn();
}
if (me.checkboxSelect) {
me.addCheckbox(view, true);
}
me.mon(view.ownerGrid, 'beforereconfigure', me.onBeforeReconfigure, me);
}
// Disable sortOnClick if we're columnSelecting
headerCt.sortOnClick = !me.getColumnSelect();
if (me.getDragSelect()) {
view.on('render', me.onViewRender, me, {
single: true
});
}
},
/**
* Initialize drag selection support
* @private
*/
onViewRender: function(view) {
var me = this,
el = view.getEl(),
views = me.views,
len = views.length,
i;
// If we receive the render event after the columnSelect config has been set,
// ensure that the view's headerCts know not to sort on click
// if we're selecting columns.
for (i = 0; i < len; i++) {
views[i].headerCt.sortOnClick = !me.columnSelect;
}
el.ddScrollConfig = {
vthresh: 50,
hthresh: 50,
frequency: 300,
increment: 100
};
Ext.dd.ScrollManager.register(el);
// Possible two child views to register as scrollable on drag
(me.scrollEls || (me.scrollEls = [])).push(el);
view.on('cellmousedown', me.handleMouseDown, me);
// In a locking situation, we need a mousedown listener on both sides.
if (view.lockingPartner) {
view.lockingPartner.on('cellmousedown', me.handleMouseDown, me);
}
},
/**
* Plumbing for drag selection of cell range
* @private
*/
handleMouseDown: function(view, td, cellIndex, record, tr, rowIdx, e) {
var me = this,
sel = me.selected,
header = e.position.column,
resumingSelection = false,
isCheckClick, startDragSelect, containsSelection;
// Ignore right click and alt modifiers.
// Also ignore touchstart because e cannot drag select using touches and
// ignore when actionableMode is true so we can select the text inside an editor
if (e.button || e.altKey || e.pointerType === 'touch' || !header) {
return;
}
me.mousedownPosition = e.position.clone();
isCheckClick = header === me.checkColumn;
if (isCheckClick) {
me.checkCellClicked = e.position.getCell(true);
}
else if (view.actionableMode) {
return;
}
// Differentiate between row and cell selections.
if (header === me.numbererColumn || isCheckClick || !me.cellSelect) {
// Enforce rowSelect setting
if (me.rowSelect) {
if (sel) {
containsSelection = sel.contains(record);
if (e.shiftKey && containsSelection) {
resumingSelection = true;
}
else if (!e.shiftKey && !e.ctrlKey && !isCheckClick) {
sel.clear();
}
}
if (!sel || !sel.isRows) {
if (sel) {
sel.clear();
}
sel = me.selected = new Ext.grid.selection.Rows(view);
}
}
else if (me.columnSelect) {
if (sel) {
containsSelection = sel.contains(me.mousedownPosition.column);
if (e.shiftKey && containsSelection) {
resumingSelection = true;
}
else if (!e.shiftKey && !e.ctrlKey && !isCheckClick) {
sel.clear();
}
}
if (!sel || !sel.isColumns) {
if (sel) {
sel.clear();
}
sel = me.selected = new Ext.grid.selection.Columns(view);
}
}
else {
return false;
}
}
else {
if (sel) {
containsSelection = sel.contains(me.getCellContext(record, cellIndex));
if (e.shiftKey && containsSelection) {
resumingSelection = true;
}
else if (!e.shiftKey) {
sel.clear();
}
}
if (!sel || !sel.isCells) {
if (sel) {
sel.clear();
}
sel = me.selected = new Ext.grid.selection.Cells(view);
}
}
startDragSelect = resumingSelection || !e.shiftKey;
if (!resumingSelection) {
if (e.shiftKey) {
return;
}
me.lastOverRecord = me.lastOverColumn = null;
}
// Add the listener after the view has potentially been corrected
me._onMouseUp = Ext.getBody().on(
'mouseup', me.onMouseUp, me, { single: true, view: sel.view, destroyable: true }
);
// Only begin the drag process if configured to select what they asked for
if (startDragSelect) {
sel.view.el.on('mousemove', me.onMouseMove, me, { view: sel.view });
}
},
/**
* Selects range based on mouse movements
* @param e
* @param target
* @param opts
* @private
*/
onMouseMove: function(e, target, opts) {
var me = this,
view = opts.view,
cell = e.getTarget(view.cellSelector),
header = opts.view.getHeaderByCell(cell),
selData = me.selected;
if (view.isLockingView) {
view = e.within(view.lockedView.el) ? view.lockedView : view.normalView;
}
// when the mousedown happens in a checkcolumn, we need to verify is the mouse pointer
// has moved out of the initial clicked cell.
// if it has, then we select the initial row and mark it as the range start,
// otherwise passing the lastOverRecord and return as we don't want
// to select the record while moving the pointer around the initial cell.
if (me.checkCellClicked) {
// We are dragging within the check cell...
if (cell === me.checkCellClicked) {
if (!me.lastOverRecord) {
me.lastOverRecord = view.getRecord(cell.parentNode);
}
return;
}
else {
me.checkCellClicked = null;
if (me.lastOverRecord) {
me.select(me.lastOverRecord);
selData.setRangeStart(view.dataSource.indexOf(me.lastOverRecord));
}
}
}
me.isDragging = true;
// Disable until a valid new selection is announced in fireSelectionChange
if (me.extensible) {
me.extensible.disable();
}
if (header) {
me.changeSelectionRange(view, cell, header, e);
}
else if (!e.within(view.body.el)) {
me.scrollTowardsPointer(e, view.ownerGrid.view);
}
},
changeSelectionRange: function(view, cell, header, e) {
var me = this,
selData = me.selected,
record, rowIdx, recChange, colChange, pos;
me.stopAutoScroller();
record = view.getRecord(cell.parentNode);
rowIdx = view.dataSource.indexOf(record);
recChange = record !== me.lastOverRecord;
colChange = header !== me.lastOverColumn;
if (recChange || colChange) {
pos = me.getCellContext(record, header);
}
// Initial mousedown was in rownumberer or checkbox column
if (selData.isRows) {
// Only react if we've changed row
if (recChange) {
if (me.lastOverRecord) {
selData.setRangeEnd(rowIdx, e.ctrlKey);
}
else {
selData.setRangeStart(rowIdx);
}
}
}
// Selecting cells
else if (selData.isCells) {
// Only react if we've changed row or column
if (recChange || colChange) {
if (me.lastOverRecord) {
selData.setRangeEnd(pos);
}
else {
selData.setRangeStart(pos);
}
}
}
// Selecting columns
else if (selData.isColumns) {
// Only react if we've changed column
if (colChange) {
if (me.lastOverColumn) {
selData.setRangeEnd(pos.column);
}
else {
selData.setRangeStart(pos.column);
}
}
}
// Focus MUST follow the mouse.
// Otherwise the focus may scroll out of the rendered range and revert to document
if (recChange || colChange) {
// We MUST pass local view into NavigationModel, not the potentially outermost
// locking view.
// TODO: When that's fixed, use setPosition(pos).
view.getNavigationModel().setPosition(
new Ext.grid.CellContext(header.getView()).setPosition(record, header)
);
}
me.lastOverColumn = header;
me.lastOverRecord = record;
},
scrollTowardsPointer: function(e, view) {
var me = this,
viewRegion = view.el.getConstrainRegion(),
point = e.getXY(),
scrollTask, scrollBy;
scrollTask = me.scrollTask || (me.scrollTask = Ext.util.TaskManager.newTask({
run: me.doAutoScroll,
args: [e, view],
scope: me,
interval: 10
}));
scrollBy = me.scrollBy || (me.scrollBy = []);
// Neart bottom of view
if (point[1] > viewRegion.bottom) {
scrollBy[0] = 0;
scrollBy[1] = 3;
scrollTask.start();
}
else if (point[1] < viewRegion.top) {
scrollBy[0] = 0;
scrollBy[1] = -3;
scrollTask.start();
}
// Near right edge of view
else if (point[0] > viewRegion.right) {
scrollBy[0] = 3;
scrollBy[1] = 0;
scrollTask.start();
}
else if (point[0] < viewRegion.left) {
scrollBy[0] = -3;
scrollBy[1] = 0;
scrollTask.start();
}
},
doAutoScroll: function(e, view) {
var me = this,
viewRegion = view.el.getConstrainRegion(),
xy = [],
cell, record, header;
if (me.destroyed) {
return;
}
// Bump the view in whatever direction was decided in the onDrag method.
if (view.scrollBy) {
view.scrollBy.apply(view, me.scrollBy);
}
if (me.scrollBy[0]) {
xy[0] = me.scrollBy[0] > 0 ? viewRegion.right - 5 : viewRegion.left + 5;
}
else {
xy[0] = e.getX();
}
if (me.scrollBy[1]) {
xy[1] = me.scrollBy[1] > 0 ? viewRegion.bottom - 5 : viewRegion.top + 5;
}
else {
xy[1] = e.getY();
}
cell = document.elementFromPoint.apply(document, xy);
if (cell) {
cell = Ext.fly(cell).up(view.cellSelector);
if (!cell) {
me.stopAutoScroller();
return;
}
record = view.getRecord(cell.dom.parentNode);
header = view.getHeaderByCell(cell.dom);
if (cell && (record !== me.lastOverRecord || header !== me.lastOverColumn)) {
me.changeSelectionRange(view, cell.dom, header, e);
}
}
},
stopAutoScroller: function() {
var me = this;
if (me.scrollTask) {
me.scrollBy[0] = me.scrollBy[1] = 0;
me.scrollTask.stop();
me.scrollTask = null;
}
},
/**
* Clean up mousemove event
* @param e
* @param target
* @param opts
* @private
*/
onMouseUp: function(e, target, opts) {
var me = this,
view = opts.view,
lastPos = me.lastOverRecord && new Ext.grid.CellContext(view).setPosition(
me.lastOverRecord, me.lastOverColumn
),
changedCell = lastPos && !lastPos.isEqual(me.mousedownPosition),
cell, record;
me.checkCellClicked = null;
me.stopAutoScroller();
if (view && !view.destroyed) {
// If we catch the event before the View sees it and stamps a position in,
// we need to know where they mouseupped.
if (!e.position) {
cell = e.getTarget(view.cellSelector);
if (cell) {
record = view.getRecord(cell);
if (record) {
e.position = new Ext.grid.CellContext(view).setPosition(
record, view.getHeaderByCell(cell)
);
}
}
}
if (e.position) {
changedCell = !e.position.isEqual(me.mousedownPosition);
}
// Disable until a valid new selection is announced in fireSelectionChange
// unless it's a click
if (me.extensible && changedCell) {
me.extensible.disable();
}
view.el.un('mousemove', me.onMouseMove, me);
// Copy the records encompassed by the drag range into the record collection
// if we are not dragging, the range will be added by onNavigate
if (me.selected.isRows && me.isDragging) {
me.selected.addRange();
}
// Fire selection change only if we have dragged - if the mouseup position
// is different from the mousedown position.
// If there has been no drag, the click handler will select the single row
if (changedCell) {
me.fireSelectionChange();
}
}
me.isDragging = false;
},
/**
* Add the header checkbox to the header row
* @param view
* @param {Boolean} initial True if we're binding for the first time.
* @private
*/
addCheckbox: function(view, initial) {
var me = this,
checkbox = me.checkboxColumnIndex,
headerCt = view.headerCt;
// Preserve behaviour of false, but not clear why that would ever be done.
if (checkbox !== false) {
if (checkbox === 'first') {
checkbox = 0;
}
else if (checkbox === 'last') {
checkbox = headerCt.getColumnCount();
}
me.checkColumn = headerCt.add(checkbox, me.getCheckboxHeaderConfig());
}
if (initial !== true) {
view.refresh();
}
},
/**
* Called when the grid's Navigation model detects navigation events (`mousedown`,
* `click` and certain `keydown` events).
* @param {Ext.event.Event} navigateEvent The event which caused navigation.
* @private
*/
onNavigate: function(navigateEvent) {
var me = this,
// Use outermost view. May be lockable
view = navigateEvent.view && navigateEvent.view.ownerGrid.view,
record = navigateEvent.record,
sel = me.selected,
// Create a new Context based upon the outermost View.
// NavigationModel works on local views.
// TODO: remove this step when NavModel is fixed to use outermost view
// in locked grid. At that point, we can use navigateEvent.position
pos = view &&
new Ext.grid.CellContext(view).setPosition(record, navigateEvent.column),
keyEvent = navigateEvent.keyEvent,
ctrlKey = keyEvent.ctrlKey,
shiftKey = keyEvent.shiftKey,
keyCode = keyEvent.getKey(),
selectionChanged, rowRangeStart, lastRecord;
// if there's no position then the user might have clicked outside a cell
if (!pos) {
return;
}
// A Column's processEvent method may set this flag if configured to do so.
if (keyEvent.stopSelection) {
return;
}
// CTRL/Arrow just navigates, does not select
if (ctrlKey && (keyCode === keyEvent.UP || keyCode === keyEvent.LEFT ||
keyCode === keyEvent.RIGHT || keyCode === keyEvent.DOWN)) {
return;
}
// Click is the mouseup at the end of a multi-cell/multi-column select swipe; reject.
if (sel && (sel.isCells || (sel.isColumns && !me.getRowSelect() && !ctrlKey)) &&
sel.getCount() > 1) {
if (shiftKey && keyEvent.type === 'click' &&
!keyEvent.position.isEqual(me.mousedownPosition)) {
return;
}
}
// If all selection types are disabled, or it's not a selecting event, return
if (!(me.cellSelect || me.columnSelect || me.rowSelect) || !navigateEvent.record ||
keyEvent.type === 'mousedown') {
return;
}
// Ctrl/A key - Deselect current selection, or select all if no selection
if (ctrlKey && keyEvent.keyCode === keyEvent.A) {
// No selection, or only one, select all
if (!sel || sel.getCount() < 2) {
me.selectAll();
}
else {
me.deselectAll();
}
me.updateHeaderState();
return;
}
if (shiftKey) {
// If the event is in one of the row selecting cells,
// or cell selecting is turned off
if (pos.column === me.numbererColumn || pos.column === me.checkColumn ||
!(me.cellSelect || me.columnSelect) || (sel && sel.isRows)) {
if (me.rowSelect) {
// Ensure selection object is of the correct type
if (!sel || !sel.isRows || sel.view !== view) {
me.resetSelection(true);
sel = me.selected = new Ext.grid.selection.Rows(view);
}
// First shift
if (!sel.getRangeSize()) {
rowRangeStart = navigateEvent.previousRecordIndex;
if (rowRangeStart == null) {
// previousRecordIndex could be empty due to BufferedRenderer
// de-rendering the last selected row.
// In that case we need to select the last selected record
// or start from 0.
lastRecord = me.getLastSelected();
rowRangeStart = lastRecord ? me.store.indexOf(lastRecord) : 0;
}
sel.setRangeStart(rowRangeStart);
}
sel.setRangeEnd(navigateEvent.recordIndex);
sel.addRange();
selectionChanged = true;
}
}
// Navigate event in a normal cell
else {
if (me.cellSelect) {
// Ensure selection object is of the correct type
if (!sel || !sel.isCells || sel.view !== view) {
me.resetSelection(true);
sel = me.selected = new Ext.grid.selection.Cells(view);
}
// First shift
if (!sel.getRangeSize()) {
sel.setRangeStart(navigateEvent.previousPosition ||
me.getCellContext(0, 0));
}
sel.setRangeEnd(pos);
selectionChanged = true;
}
else if (me.columnSelect) {
// Ensure selection object is of the correct type
if (!sel || !sel.isColumns || sel.view !== view) {
me.resetSelection(true);
sel = me.selected = new Ext.grid.selection.Columns(view);
}
if (!sel.getCount()) {
sel.setRangeStart(pos.column);
}
sel.setRangeEnd(navigateEvent.position.column);
selectionChanged = true;
}
}
}
else {
// If the event is in one of the row selecting cells, or we have enabled
// row selection but not column selection so prioritize selecting rows
if (pos.column === me.numbererColumn || pos.column === me.checkColumn ||
(me.rowSelect && !me.cellSelect)) {
// Ensure selection object is of the correct type
if (!sel || !sel.isRows || sel.view !== view) {
me.resetSelection(true);
sel = me.selected = new Ext.grid.selection.Rows(view);
}
if (ctrlKey || pos.column === me.checkColumn) {
if (sel.contains(record)) {
sel.remove(record);
}
else {
sel.add(record);
}
}
else {
sel.clear();
sel.add(record);
sel.setRangeStart(pos.rowIdx, true);
}
selectionChanged = true;
}
// Navigate event in a normal cell
else if (keyEvent.getTarget(me.view.getCellSelector())) {
// Prioritize cell selection over column selection, also we have to make sure
// we only handle events that were fired by a cellClick.
// If an itemclick (row selection) was fired due to dragging,
// it will be handled by the selection#setRangeEnd method.
if (me.cellSelect) {
// Ensure selection object is of the correct type
if (!sel || !sel.isCells || sel.view !== view) {
me.resetSelection(true);
me.selected = sel = new Ext.grid.selection.Cells(view);
}
else {
sel.clear();
}
sel.setRangeStart(pos);
selectionChanged = true;
}
else if (me.columnSelect) {
// Ensure selection object is of the correct type
if (!sel || !sel.isColumns || sel.view !== view) {
me.resetSelection(true);
me.selected = sel = new Ext.grid.selection.Columns(view);
}
if (ctrlKey) {
if (sel.contains(pos.column)) {
sel.remove(pos.column);
}
else {
sel.add(pos.column);
}
}
else {
sel.setRangeStart(pos.column);
}
selectionChanged = true;
}
}
}
// If our configuration allowed selection changes, update check header and fire event
if (selectionChanged) {
if (sel.isRows) {
me.updateHeaderState();
}
// this will give continuity between keyboard selection and mouse selection
me.lastOverRecord = record;
me.lastOverColumn = pos.column;
me.fireSelectionChange();
}
},
/**
* Checks the current selection (if available) against the context being removed.
* If the context was selected, the selection is cleared since it's no longer valid.
* @param {Object} removedContext
* @return {Boolean} `true` if part or all of the selection was cleared
* @since 6.2.2
*/
maybeClearSelection: function(removedContext) {
var me = this,
selData = me.selected,
startCell = selData.startCell,
endCell = selData.endCell,
column = removedContext.column,
colIdx = removedContext.colIdx,
rowIdx = removedContext.rowIdx,
changed;
if (startCell && (startCell.column === column || startCell.colIdx === colIdx) &&
startCell.rowIdx === rowIdx) {
selData.startCell = changed = null;
}
if (endCell && (endCell.column === column || endCell.colIdx === colIdx) &&
endCell.rowIdx === rowIdx) {
selData.endCell = changed = null;
}
return changed === null;
},
/**
* Check if given record is currently selected.
*
* Used in {@link Ext.view.Table view} rendering to decide upon cell UI treatment.
* @param {Ext.data.Model} record
* @return {Boolean}
* @private
*/
isRowSelected: function(record) {
var me = this,
sel = me.selected;
if (sel && sel.isRows) {
record = Ext.isNumber(record) ? me.store.getAt(record) : record;
return sel.contains(record);
}
else {
return false;
}
},
/**
* Check if given column is currently selected.
*
* @param {Ext.grid.column.Column} column
* @return {Boolean}
* @private
*/
isColumnSelected: function(column) {
var me = this,
sel = me.selected;
if (sel && sel.isColumns) {
return sel.contains(column);
}
else {
return false;
}
},
/**
* Returns true if specified cell within specified view is selected
*
* Used in {@link Ext.view.Table view} rendering to decide upon row UI treatment.
* @param {Ext.grid.View} view - impactful when locked columns are used
* @param {Number} row - row index
* @param {Number} column - column index, within the current view
*
* @return {Boolean}
* @private
*/
isCellSelected: function(view, row, column) {
var me = this,
testPos,
sel = me.selected;
// view MUST be outermost (possible locking) view
view = view.ownerGrid.view;
if (sel) {
if (sel.isColumns) {
if (typeof column === 'number') {
column = view.getVisibleColumnManager().getColumns()[column];
}
return sel.contains(column);
}
if (sel.isCells) {
testPos = new Ext.grid.CellContext(view).setPosition({
row: row,
// IMPORTANT: The historic API for columns has been to include
// hidden columns in the index.
// So we must index into the "all" ColumnManager.
column: column
});
return sel.contains(testPos);
}
}
return false;
},
/**
* @private
*/
applySelected: function(selected) {
// Must override base class's applier which creates a Collection
//<debug>
if (selected && !(selected.isRows || selected.isCells || selected.isColumns)) {
Ext.raise('SpreadsheelModel#setSelected must be passed an instance ' +
'of Ext.grid.selection.Selection');
}
//</debug>
return selected;
},
/**
* @private
*/
updateSelected: function(selected, oldSelected) {
var view,
columns,
len,
i,
cell;
// Clear old selection.
if (oldSelected) {
oldSelected.clear();
}
// Update the UI to match the new selection
if (selected && selected.getCount()) {
view = selected.view;
// Rows; update each selected row
if (selected.isRows) {
selected.eachRow(view.onRowSelect, view);
}
// Columns; update the selected columns for all rows
else if (selected.isColumns) {
columns = selected.getColumns();
len = columns.length;
if (len) {
cell = new Ext.grid.CelContext(view);
view.store.each(function(rec) {
cell.setRow(rec);
for (i = 0; i < len; i++) {
cell.setColumn(columns[i]);
view.onCellSelect(cell);
}
});
}
}
// Cells; update each selected cell
else if (selected.isCells) {
selected.eachCell(view.onCellSelect, view);
}
}
},
getNumbererColumn: function(col) {
var me = this,
result = me.numbererColumn,
view = me.view;
if (!result) {
// Always put row selection columns in the locked side if there is one.
if (view.isNormalView) {
view = view.ownerGrid.lockedGrid;
}
result = me.numbererColumn = view.headerCt.down('rownumberer') ||
view.headerCt.add(0, me.getNumbererColumnConfig());
}
return result;
},
/**
* Show/hide the extra column headers depending upon rowSelection.
* @private
*/
updateRowSelect: function(rowSelect) {
var me = this,
sel = me.selected,
view = me.view;
if (view && view.rendered) {
if (rowSelect) {
if (me.checkColumn) {
me.checkColumn.show();
}
me.getNumbererColumn().show();
}
else {
if (me.checkColumn) {
me.checkColumn.hide();
}
if (me.numbererColumn) {
me.numbererColumn.hide();
}
}
if (!rowSelect && sel && sel.isRows) {
sel.clear();
me.fireSelectionChange();
}
}
},
/**
* Enable/disable the HeaderContainer's sortOnClick in line with column select on
* column click.
* @private
*/
updateColumnSelect: function(columnSelect) {
var me = this,
sel = me.selected,
views = me.views,
len = views ? views.length : 0,
i;
for (i = 0; i < len; i++) {
views[i].headerCt.sortOnClick = !columnSelect;
}
if (!columnSelect && sel && sel.isColumns) {
sel.clear();
me.fireSelectionChange();
}
if (columnSelect) {
me.view.ownerGrid.addCls(me.columnSelectCls);
}
else {
me.view.ownerGrid.removeCls(me.columnSelectCls);
}
},
/**
* @private
*/
updateCellSelect: function(cellSelect) {
var me = this,
sel = me.selected;
if (!cellSelect && sel && sel.isCells) {
sel.clear();
me.fireSelectionChange();
}
},
/**
* @private
*/
fireSelectionChange: function() {
var me = this,
sel = me.selected,
view = sel.view,
grid = view.ownerGrid,
store = view.dataSource,
records, count;
// Inform selection object that we're done
me.updateSelectionExtender();
// We must still fire a selectionchange event through the SelectionModel
// because Ext.panel.Table listens for this event to update its bound selection.
if (sel.isRows) {
records = sel.getRecords();
count = store.getTotalCount() || store.getCount();
// When there is a BufferedStore the allSelected flag cannot be set
// in a manual selection
// eslint-disable-next-line max-len
me.selected.allSelected = !!(store.isBufferedStore ? me.selected.allSelected : count && records.length && (count === records.length));
me.fireEvent('selectionchange', me, records);
}
else if (sel.isCells) {
me.selected.allSelected = false;
// eslint-disable-next-line max-len
me.fireEvent('selectionchange', me, sel.getCount() ? me.store.getRange.apply(sel.view.dataSource, sel.getRowRange()) : []);
}
grid.fireEvent('selectionchange', grid, sel);
},
/**
* @private
* Called by {@link Ext.panel.Table#updateBindSelection} when publishing the `selection`
* property. It should yield the last record selected.
* @return {Ext.data.Model} The last record selected. This is only available
* if the current selection type is cells or rows.
* In the case of multiple selection, the *last* record added to the selection is returned.
*/
getLastSelected: function() {
var sel = this.selected;
if (sel.getLastSelected) {
return sel.getLastSelected();
}
},
updateSelectionExtender: function() {
var sel = this.selected;
if (sel) {
sel.onSelectionFinish();
}
},
/**
* Called when a selection has been made. The selection object's onSelectionFinish
* calls back into this.
* @param {Ext.grid.selection.Selection} sel The selection object specific to
* the selection performed.
* @param {Ext.grid.CellContext} [firstCell] The left/top most selected cell.
* Will be undefined if the selection is clear.
* @param {Ext.grid.CellContext} [lastCell] The bottom/right most selected cell.
* Will be undefined if the selection is clear.
* @private
*/
onSelectionFinish: function(sel, firstCell, lastCell) {
var extensible = this.getExtensible();
if (extensible) {
extensible.setHandle(firstCell, lastCell);
}
},
applyExtensible: function(extensible) {
var me = this;
// if extensible is false/null we should return undefined so the value
// does not get set and we don't call updateExtensible
if (!extensible) {
return undefined;
}
if (extensible === true || typeof extensible === 'string') {
extensible = {
axes: me.axesConfigs[extensible]
};
}
else {
extensible = Ext.Object.chain(extensible); // don't mutate the user's config
}
extensible.allowReduceSelection = me.getReducible();
extensible.view = me.selected.view;
return new Ext.grid.selection.SelectionExtender(extensible);
},
/*
* @private
*/
applyReducible: function(reducible) {
return !!reducible;
},
updateReducible: function(reducible) {
// do not call getExtensible() here to avoid creation
var extensible = this.extensible;
if (extensible) {
extensible.allowReduceSelection = reducible;
}
},
/**
* Called when the SelectionExtender has the mouse released.
* @param {Object} extension An object describing the type and size of extension.
* @param {String} extension.type `"rows"` or `"columns"`
* @param {Ext.grid.CellContext} extension.start The start (top left) cell of the
* extension area.
* @param {Ext.grid.CellContext} extension.end The end (bottom right) cell of the
* extension area.
* @param {number} [extension.columns] The number of columns extended (-ve means
* on the left side).
* @param {number} [extension.rows] The number of rows extended (-ve means on the top side).
* @private
*/
extendSelection: function(extension) {
var me = this,
sel = me.selected,
action = extension.reduce ? 'reduce' : 'extend';
// Announce that the selection is to be extended, and if no objections, extend it
// eslint-disable-next-line max-len
if (me.view.ownerGrid.fireEvent('beforeselectionextend', me.view.ownerGrid, sel, extension) !== false) {
sel[action + 'Range'](extension);
me.fireSelectionChange();
}
},
/**
* @private
*/
onIdChanged: function(store, rec, oldId, newId) {
var sel = this.selected;
if (sel && sel.isRows && sel.selectedRecords) {
sel.selectedRecords.updateKey(rec, oldId);
}
},
/**
* Called when a page is added to BufferedStore.
* @private
*/
onPageAdd: function(pageMap, pageNumber, records) {
var sel = this.selected,
len = records.length,
i,
record,
selected = sel && sel.selectedRecords;
// Check for return of already selected records.
// Maintainer: To only use one conditional expression, the value of assignment of
// (selected = sel.selectedRecords) is part of the single conditional expression.
if (selected && sel.isRows) {
for (i = 0; i < len; i++) {
record = records[i];
if (selected.get(record.id)) {
selected.replace(record);
}
else if (sel.allSelected) {
selected.add(record);
}
}
}
},
/**
* @private
*/
refresh: function() {
var sel = this.getSelected();
// Refreshing the selected record Collection based upon a possible
// store mutation is only valid if we are selecting records.
if (sel && sel.isRows) {
this.callParent();
}
},
/**
* @private
*/
onStoreAdd: function() {
var sel = this.getSelected();
// Updating on store mutation is only valid if we are selecting records.
if (sel && sel.isRows) {
this.callParent(arguments);
this.updateHeaderState();
}
},
/**
* @private
*/
onStoreClear: function() {
this.resetSelection();
},
/**
* @private
*/
onStoreLoad: function() {
var sel = this.getSelected();
// Updating on store mutation is only valid if we are selecting records.
if (sel && sel.isRows) {
this.callParent(arguments);
this.updateHeaderState();
}
},
/**
* @private
*/
onStoreRefresh: function() {
var sel = this.selected;
// Ensure that records which are no longer in the new store are pruned
// if configured to do so.
// Ensure that selected records in the collection are the correct instance.
if (sel && sel.isRows && sel.selectedRecords) {
this.updateSelectedInstances(sel.selectedRecords);
}
if (this.view) {
this.updateHeaderState();
}
},
/**
* @private
*/
onPageRemove: function(pageMap, pageNumber, records) {
var sel = this.selected;
// On page purge from a buffered store, do not react if
// we have selected all. All are still selected!
if (!(sel && sel.allSelected)) {
this.onStoreRemove(this.store, records);
}
},
/**
* @private
*/
onStoreRemove: function() {
var sel = this.getSelected();
// Updating on store mutation is only valid if we are selecting records.
if (sel && sel.isRows) {
this.callParent(arguments);
}
}
}
}, function(SpreadsheetModel) {
var RowNumberer = Ext.ClassManager.get('Ext.grid.column.RowNumberer');
if (RowNumberer) {
SpreadsheetModel.prototype.rowNumbererTdCls =
Ext.grid.column.RowNumberer.prototype.tdCls + ' ' + Ext.baseCSSPrefix +
'ssm-row-numberer-cell';
}
});