/**
* @class Ext.selection.Model
* @extends Ext.util.Observable
*
* Tracks what records are currently selected in a databound widget.
*
* This is an abstract class and is not meant to be directly used.
*
* DataBound UI widgets such as GridPanel, TreePanel, and ListView
* should subclass AbstractStoreSelectionModel and provide a way
* to binding to the component.
*
* The abstract methods onSelectChange and onLastFocusChanged should
* be implemented in these subclasses to update the UI widget.
*/
Ext.define('Ext.selection.Model', {
extend: 'Ext.util.Observable',
alternateClassName: 'Ext.AbstractStoreSelectionModel',
requires: ['Ext.data.StoreMgr'],
// lastSelected
/**
* @cfg {String} mode
* Modes of selection.
* Valid values are SINGLE, SIMPLE, and MULTI. Defaults to 'SINGLE'
*/
/**
* @cfg {Boolean} allowDeselect
* Allow users to deselect a record in a DataView, List or Grid. Only applicable when the SelectionModel's mode is 'SINGLE'. Defaults to false.
*/
allowDeselect: false,
/**
* @property selected
* READ-ONLY A MixedCollection that maintains all of the currently selected
* records.
*/
selected: null,
constructor: function(cfg) {
var me = this;
cfg = cfg || {};
Ext.apply(me, cfg);
me.addEvents(
/**
* @event selectionchange
* Fired after a selection change has occurred
* @param {Ext.selection.Model} this
* @param {Array} selected The selected records
*/
'selectionchange'
);
me.modes = {
SINGLE: true,
SIMPLE: true,
MULTI: true
};
// sets this.selectionMode
me.setSelectionMode(cfg.mode || me.mode);
// maintains the currently selected records.
me.selected = new Ext.util.MixedCollection();
Ext.selection.Model.superclass.constructor.call(me, cfg);
},
// binds the store to the selModel.
bind : function(store, initial){
var me = this;
if(!initial && me.store){
if(store !== me.store && me.store.autoDestroy){
me.store.destroy();
}else{
me.store.un("add", me.onStoreAdd, me);
me.store.un("clear", me.onStoreClear, me);
me.store.un("remove", me.onStoreRemove, me);
me.store.un("update", me.onStoreUpdate, me);
}
}
if(store){
store = Ext.data.StoreMgr.lookup(store);
store.on({
add: me.onStoreAdd,
clear: me.onStoreClear,
remove: me.onStoreRemove,
update: me.onStoreUpdate,
scope: me
});
}
me.store = store;
if(store && !initial) {
me.refresh();
}
},
selectAll: function(silent) {
var selections = this.store.getRange(),
i = 0,
len = selections.length;
for (; i < len; i++) {
this.doSelect(selections[i], true, silent);
}
},
deselectAll: function() {
var selections = this.getSelection(),
i = 0,
len = selections.length;
for (; i < len; i++) {
this.doDeselect(selections[i]);
}
},
// Provides differentiation of logic between MULTI, SIMPLE and SINGLE
// selection modes. Requires that an event be passed so that we can know
// if user held ctrl or shift.
selectWithEvent: function(record, e) {
var me = this;
switch (me.selectionMode) {
case 'MULTI':
if (e.ctrlKey && me.isSelected(record)) {
me.doDeselect(record, false);
} else if (e.shiftKey && me.lastFocused) {
me.selectRange(me.lastFocused, record, e.ctrlKey);
} else if (e.ctrlKey) {
me.doSelect(record, true, false);
} else if (me.isSelected(record) && !e.shiftKey && !e.ctrlKey && me.selected.getCount() > 1) {
me.doSelect(record, false, false);
} else {
me.doSelect(record, false);
}
break;
case 'SIMPLE':
if (me.isSelected(record)) {
me.doDeselect(record);
} else {
me.doSelect(record, true);
}
break;
case 'SINGLE':
// if allowDeselect is on and this record isSelected, deselect it
if (me.allowDeselect && me.isSelected(record)) {
me.doDeselect(record);
// select the record and do NOT maintain existing selections
} else {
me.doSelect(record, false);
}
break;
}
},
/**
* Selects a range of rows if the selection model
* {@link Ext.grid.AbstractSelectionModel#isLocked is not locked}.
* All rows in between startRow and endRow are also selected.
* @param {Number} startRow The index of the first row in the range
* @param {Number} endRow The index of the last row in the range
* @param {Boolean} keepExisting (optional) True to retain existing selections
*/
selectRange : function(startRecord, endRecord, keepExisting, dir){
var me = this,
store = me.store,
startRow = store.indexOf(startRecord),
endRow = store.indexOf(endRecord),
selectedCount = 0,
i,
tmp,
dontDeselect;
if (me.isLocked()){
return;
}
// swap values
if (startRow > endRow){
tmp = endRow;
endRow = startRow;
startRow = tmp;
}
for (i = startRow; i <= endRow; i++) {
if (me.isSelected(store.getAt(i))) {
selectedCount++;
}
}
if (!dir) {
dontDeselect = -1;
} else {
dontDeselect = (dir == 'up') ? startRow : endRow;
}
for (i = startRow; i <= endRow; i++){
if (selectedCount == (endRow - startRow + 1)) {
if (i != dontDeselect) {
me.doDeselect(i, true);
}
} else {
me.doSelect(i, true);
}
}
},
/**
* Selects a record instance by record instance or index.
* @param {Ext.data.Record/Index} records An array of records or an index
* @param {Boolean} keepExisting
* @param {Boolean} suppressEvent Set to false to not fire a select event
*/
select: function(records, keepExisting, suppressEvent) {
this.doSelect(records, keepExisting, suppressEvent);
},
/**
* Deselects a record instance by record instance or index.
* @param {Ext.data.Record/Index} records An array of records or an index
* @param {Boolean} suppressEvent Set to false to not fire a deselect event
*/
deselect: function(records, suppressEvent) {
this.doDeselect(records, suppressEvent);
},
doSelect: function(records, keepExisting, suppressEvent) {
var me = this,
record;
if (me.locked) {
return;
}
if (typeof records === "number") {
records = [me.store.getAt(records)];
}
if (me.selectionMode == "SINGLE" && records) {
record = records.length ? records[0] : records;
me.doSingleSelect(record, suppressEvent);
} else {
me.doMultiSelect(records, keepExisting, suppressEvent);
}
},
doMultiSelect: function(records, keepExisting, suppressEvent) {
var me = this,
selected = me.selected,
change = false,
i = 0,
len, record;
if (me.locked) {
return;
}
records = !Ext.isArray(records) ? [records] : records;
len = records.length;
if (!keepExisting && selected.getCount() > 0) {
change = true;
me.doDeselect(me.getSelection(), true);
}
for (; i < len; i++) {
record = records[i];
if (keepExisting && me.isSelected(record)) {
continue;
}
change = true;
me.lastSelected = record;
selected.add(record);
if (!suppressEvent) {
me.setLastFocused(record);
}
me.onSelectChange(record, true, suppressEvent);
}
// fire selchange if there was a change and there is no suppressEvent flag
me.maybeFireSelectionChange(change && !suppressEvent);
},
// records can be an index, a record or an array of records
doDeselect: function(records, suppressEvent) {
var me = this,
selected = me.selected,
change = false,
i = 0,
len, record;
if (me.locked) {
return;
}
if (typeof records === "number") {
records = [me.store.getAt(records)];
}
records = !Ext.isArray(records) ? [records] : records;
len = records.length;
for (; i < len; i++) {
record = records[i];
if (selected.remove(record)) {
if (me.lastSelected == record) {
me.lastSelected = selected.last();
}
me.onSelectChange(record, false, suppressEvent);
change = true;
}
}
// fire selchange if there was a change and there is no suppressEvent flag
me.maybeFireSelectionChange(change && !suppressEvent);
},
doSingleSelect: function(record, suppressEvent) {
var me = this,
selected = me.selected;
if (me.locked) {
return;
}
// already selected.
// should we also check beforeselect?
if (me.isSelected(record)) {
return;
}
if (selected.getCount() > 0) {
me.doDeselect(me.lastSelected, suppressEvent);
}
selected.add(record);
me.lastSelected = record;
me.onSelectChange(record, true, suppressEvent);
me.setLastFocused(record);
me.maybeFireSelectionChange(!suppressEvent);
},
/**
* @param {Ext.data.Record} record
* Set a record as the last focused record. This does NOT mean
* that the record has been selected.
*/
setLastFocused: function(record) {
var me = this,
recordBeforeLast = me.lastFocused;
me.lastFocused = record;
me.onLastFocusChanged(recordBeforeLast, record);
},
// fire selection change as long as true is not passed
// into maybeFireSelectionChange
maybeFireSelectionChange: function(fireEvent) {
if (fireEvent) {
var me = this;
me.fireEvent('selectionchange', me, me.getSelection());
}
},
/**
* Returns the last selected record.
*/
getLastSelected: function() {
return this.lastSelected;
},
getLastFocused: function() {
return this.lastFocused;
},
/**
* Returns an array of the currently selected records.
*/
getSelection: function() {
return this.selected.getRange();
},
/**
* Returns the current selectionMode. SINGLE, MULTI or SIMPLE.
*/
getSelectionMode: function() {
return this.selectionMode;
},
/**
* Sets the current selectionMode. SINGLE, MULTI or SIMPLE.
*/
setSelectionMode: function(selMode) {
selMode = selMode ? selMode.toUpperCase() : 'SINGLE';
// set to mode specified unless it doesnt exist, in that case
// use single.
this.selectionMode = this.modes[selMode] ? selMode : 'SINGLE';
},
/**
* Returns true if the selections are locked.
* @return {Boolean}
*/
isLocked: function() {
return this.locked;
},
/**
* Locks the current selection and disables any changes from
* happening to the selection.
* @param {Boolean} locked
*/
setLocked: function(locked) {
this.locked = !!locked;
},
/**
* Returns true if the specified row is selected.
* @param {Record/Number} record The record or index of the record to check
* @return {Boolean}
*/
isSelected: function(record) {
record = Ext.isNumber(record) ? this.store.getAt(record) : record;
return this.selected.indexOf(record) !== -1;
},
/**
* Returns true if there is a selected record.
* @return {Boolean}
*/
hasSelection: function() {
return this.selected.getCount() > 0;
},
refresh: function() {
var me = this,
toBeSelected = [],
oldSelections = me.getSelection(),
len = oldSelections.length,
selection,
change,
i = 0;
// check to make sure that there are no records
// missing after the refresh was triggered, prune
// them from what is to be selected if so
for (; i < len; i++) {
selection = oldSelections[i];
if (me.store.indexOf(selection) != -1) {
toBeSelected.push(selection);
}
}
// there was a change from the old selected and
// the new selection
if (me.selected.getCount() != toBeSelected.length) {
change = true;
}
me.clearSelections();
if (toBeSelected.length) {
// perform the selection again
me.doSelect(toBeSelected, false, true);
}
me.maybeFireSelectionChange(change);
},
clearSelections: function() {
// reset the entire selection to nothing
var me = this;
me.selected.clear();
me.lastSelected = null;
me.setLastFocused(null);
},
// when a record is added to a store
onStoreAdd: function() {
},
// when a store is cleared remove all selections
// (if there were any)
onStoreClear: function() {
var me = this,
selected = this.selected;
if (selected.getCount > 0) {
selected.clear();
me.lastSelected = null;
me.setLastFocused(null);
me.maybeFireSelectionChange(true);
}
},
// prune records from the SelectionModel if
// they were selected at the time they were
// removed.
onStoreRemove: function(store, record) {
var me = this,
selected = me.selected;
if (me.locked) {
return;
}
if (selected.remove(record)) {
if (me.lastSelected == record) {
me.lastSelected = null;
}
if (me.getLastFocused() == record) {
me.setLastFocused(null);
}
me.maybeFireSelectionChange(true);
}
},
getCount: function() {
return this.selected.getCount();
},
// cleanup.
destroy: function() {
},
// if records are updated
onStoreUpdate: function() {
},
// @abstract
onSelectChange: function(record, isSelected, suppressEvent) {
},
// @abstract
onLastFocusChanged: function(oldFocused, newFocused) {
},
// @abstract
onEditorKey: function(field, e) {
},
// @abstract
bindComponent: function(cmp) {
}
});