--- /dev/null
+/**
+ * @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.StoreManager'],
+ // 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,
+
+
+ /**
+ * Prune records when they are removed from the store from the selection.
+ * This is a private flag. For an example of its usage, take a look at
+ * Ext.selection.TreeModel.
+ * @private
+ */
+ pruneRemoved: true,
+
+ 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 = Ext.create('Ext.util.MixedCollection');
+
+ me.callParent(arguments);
+ },
+
+ // 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.StoreManager.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 #isLocked is not locked}.
+ * All rows in between startRow and endRow are also selected.
+ * @param {Ext.data.Model/Number} startRow The record or index of the first row in the range
+ * @param {Ext.data.Model/Number} endRow The record or index of the last row in the range
+ * @param {Boolean} keepExisting (optional) True to retain existing selections
+ */
+ selectRange : function(startRow, endRow, keepExisting, dir){
+ var me = this,
+ store = me.store,
+ selectedCount = 0,
+ i,
+ tmp,
+ dontDeselect,
+ records = [];
+
+ if (me.isLocked()){
+ return;
+ }
+
+ if (!keepExisting) {
+ me.clearSelections();
+ }
+
+ if (!Ext.isNumber(startRow)) {
+ startRow = store.indexOf(startRow);
+ }
+ if (!Ext.isNumber(endRow)) {
+ endRow = store.indexOf(endRow);
+ }
+
+ // 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 {
+ records.push(store.getAt(i));
+ }
+ }
+ me.doMultiSelect(records, true);
+ },
+
+ /**
+ * Selects a record instance by record instance or index.
+ * @param {Ext.data.Model/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.Model/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);
+
+ me.onSelectChange(record, true, suppressEvent);
+ }
+ me.setLastFocused(record, 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);
+ if (!suppressEvent) {
+ me.setLastFocused(record);
+ }
+ me.maybeFireSelectionChange(!suppressEvent);
+ },
+
+ /**
+ * @param {Ext.data.Model} record
+ * Set a record as the last focused record. This does NOT mean
+ * that the record has been selected.
+ */
+ setLastFocused: function(record, supressFocus) {
+ var me = this,
+ recordBeforeLast = me.lastFocused;
+ me.lastFocused = record;
+ me.onLastFocusChanged(recordBeforeLast, record, supressFocus);
+ },
+
+ /**
+ * Determines if this record is currently focused.
+ * @param Ext.data.Record record
+ */
+ isFocused: function(record) {
+ return record === this.getLastFocused();
+ },
+
+
+ // 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 <tt>true</tt> 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,
+ lastFocused = this.getLastFocused();
+
+ // 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 (!this.pruneRemoved || 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 (me.store.indexOf(lastFocused) !== -1) {
+ // restore the last focus but supress restoring focus
+ this.setLastFocused(lastFocused, true);
+ }
+
+ 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 || !me.pruneRemoved) {
+ 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) {
+
+ }
+});
\ No newline at end of file