Upgrade to ExtJS 4.0.7 - Released 10/19/2011
[extjs.git] / src / selection / Model.js
index 6ba20ae..c3a3d28 100644 (file)
@@ -1,44 +1,57 @@
+/*
+
+This file is part of Ext JS 4
+
+Copyright (c) 2011 Sencha Inc
+
+Contact:  http://www.sencha.com/contact
+
+GNU General Public License Usage
+This file may be used under the terms of the GNU General Public License version 3.0 as published by the Free Software Foundation and appearing in the file LICENSE included in the packaging of this file.  Please review the following information to ensure the GNU General Public License version 3.0 requirements will be met: http://www.gnu.org/copyleft/gpl.html.
+
+If you are unsure which license is appropriate for your use, please contact the sales department at http://www.sencha.com/contact.
+
+*/
 /**
- * @class Ext.selection.Model
- * @extends Ext.util.Observable
- *
- * Tracks what records are currently selected in a databound widget.
+ * Tracks what records are currently selected in a databound component.
  *
- * This is an abstract class and is not meant to be directly used.
+ * This is an abstract class and is not meant to be directly used. Databound UI widgets such as
+ * {@link Ext.grid.Panel Grid} and {@link Ext.tree.Panel Tree} should subclass Ext.selection.Model
+ * and provide a way to binding to the component.
  *
- * 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.
+ * 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',
+    alternateClassName: 'Ext.AbstractSelectionModel',
     requires: ['Ext.data.StoreManager'],
     // lastSelected
 
     /**
      * @cfg {String} mode
-     * Modes of selection.
-     * Valid values are SINGLE, SIMPLE, and MULTI. Defaults to 'SINGLE'
+     * Mode of selection.  Valid values are:
+     *
+     * - **SINGLE** - Only allows selecting one item at a time.  Use {@link #allowDeselect} to allow
+     *   deselecting that item.  This is the default.
+     * - **SIMPLE** - Allows simple selection of multiple items one-by-one. Each click in grid will either
+     *   select or deselect an item.
+     * - **MULTI** - Allows complex selection of multiple items using Ctrl and Shift keys.
      */
-    
+
     /**
      * @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.
+     * Allow users to deselect a record in a DataView, List or Grid.
+     * Only applicable when the {@link #mode} is 'SINGLE'.
      */
     allowDeselect: false,
 
     /**
-     * @property selected
-     * READ-ONLY A MixedCollection that maintains all of the currently selected
-     * records.
+     * @property {Ext.util.MixedCollection} selected
+     * A MixedCollection that maintains all of the currently selected records. Read-only.
      */
     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
@@ -49,18 +62,18 @@ Ext.define('Ext.selection.Model', {
 
     constructor: function(cfg) {
         var me = this;
-        
+
         cfg = cfg || {};
         Ext.apply(me, cfg);
-        
+
         me.addEvents(
             /**
-             * @event selectionchange
+             * @event
              * Fired after a selection change has occurred
              * @param {Ext.selection.Model} this
-             * @param  {Array} selected The selected records
+             * @param {Ext.data.Model[]} selected The selected records
              */
-             'selectionchange'
+            'selectionchange'
         );
 
         me.modes = {
@@ -74,17 +87,17 @@ Ext.define('Ext.selection.Model', {
 
         // 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();
+                me.store.destroyStore();
             }else{
                 me.store.un("add", me.onStoreAdd, me);
                 me.store.un("clear", me.onStoreClear, me);
@@ -109,8 +122,8 @@ Ext.define('Ext.selection.Model', {
     },
 
     /**
-     * Select all records in the view.
-     * @param {Boolean} suppressEvent True to suppress any selects event
+     * Selects all records in the view.
+     * @param {Boolean} suppressEvent True to suppress any select events
      */
     selectAll: function(suppressEvent) {
         var me = this,
@@ -118,7 +131,7 @@ Ext.define('Ext.selection.Model', {
             i = 0,
             len = selections.length,
             start = me.getSelection().length;
-            
+
         me.bulkChange = true;
         for (; i < len; i++) {
             me.doSelect(selections[i], true, suppressEvent);
@@ -129,7 +142,7 @@ Ext.define('Ext.selection.Model', {
     },
 
     /**
-     * Deselect all records in the view.
+     * Deselects all records in the view.
      * @param {Boolean} suppressEvent True to suppress any deselect events
      */
     deselectAll: function(suppressEvent) {
@@ -138,7 +151,7 @@ Ext.define('Ext.selection.Model', {
             i = 0,
             len = selections.length,
             start = me.getSelection().length;
-            
+
         me.bulkChange = true;
         for (; i < len; i++) {
             me.doDeselect(selections[i], suppressEvent);
@@ -151,9 +164,9 @@ Ext.define('Ext.selection.Model', {
     // 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) {
+    selectWithEvent: function(record, e, keepExisting) {
         var me = this;
-        
+
         switch (me.selectionMode) {
             case 'MULTI':
                 if (e.ctrlKey && me.isSelected(record)) {
@@ -163,7 +176,7 @@ Ext.define('Ext.selection.Model', {
                 } 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);
+                    me.doSelect(record, keepExisting, false);
                 } else {
                     me.doSelect(record, false);
                 }
@@ -192,7 +205,7 @@ Ext.define('Ext.selection.Model', {
      * 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
+     * @param {Boolean} [keepExisting] True to retain existing selections
      */
     selectRange : function(startRow, endRow, keepExisting, dir){
         var me = this,
@@ -202,22 +215,22 @@ Ext.define('Ext.selection.Model', {
             tmp,
             dontDeselect,
             records = [];
-        
+
         if (me.isLocked()){
             return;
         }
-        
+
         if (!keepExisting) {
-            me.clearSelections();
+            me.deselectAll(true);
         }
-        
+
         if (!Ext.isNumber(startRow)) {
             startRow = store.indexOf(startRow);
-        } 
+        }
         if (!Ext.isNumber(endRow)) {
             endRow = store.indexOf(endRow);
         }
-        
+
         // swap values
         if (startRow > endRow){
             tmp = endRow;
@@ -236,7 +249,7 @@ Ext.define('Ext.selection.Model', {
         } else {
             dontDeselect = (dir == 'up') ? startRow : endRow;
         }
-        
+
         for (i = startRow; i <= endRow; i++){
             if (selectedCount == (endRow - startRow + 1)) {
                 if (i != dontDeselect) {
@@ -248,30 +261,33 @@ Ext.define('Ext.selection.Model', {
         }
         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
+     * @param {Ext.data.Model[]/Number} records An array of records or an index
+     * @param {Boolean} [keepExisting] True to retain existing selections
+     * @param {Boolean} [suppressEvent] Set to true to not fire a select event
      */
     select: function(records, keepExisting, suppressEvent) {
-        this.doSelect(records, keepExisting, suppressEvent);
+        // Automatically selecting eg store.first() or store.last() will pass undefined, so that must just return;
+        if (Ext.isDefined(records)) {
+            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
+     * @param {Ext.data.Model[]/Number} records An array of records or an index
+     * @param {Boolean} [suppressEvent] Set to true 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;
         }
@@ -292,17 +308,24 @@ Ext.define('Ext.selection.Model', {
             change = false,
             i = 0,
             len, record;
-            
+
         if (me.locked) {
             return;
         }
-        
+
 
         records = !Ext.isArray(records) ? [records] : records;
         len = records.length;
         if (!keepExisting && selected.getCount() > 0) {
+            if (me.doDeselect(me.getSelection(), suppressEvent) === false) {
+                return;
+            }
+            // TODO - coalesce the selectionchange event in deselect w/the one below...
+        }
+
+        function commit () {
+            selected.add(record);
             change = true;
-            me.doDeselect(me.getSelection(), suppressEvent);
         }
 
         for (; i < len; i++) {
@@ -310,11 +333,9 @@ Ext.define('Ext.selection.Model', {
             if (keepExisting && me.isSelected(record)) {
                 continue;
             }
-            change = true;
             me.lastSelected = record;
-            selected.add(record);
 
-            me.onSelectChange(record, true, suppressEvent);
+            me.onSelectChange(record, true, suppressEvent, commit);
         }
         me.setLastFocused(record, suppressEvent);
         // fire selchange if there was a change and there is no suppressEvent flag
@@ -325,38 +346,49 @@ Ext.define('Ext.selection.Model', {
     doDeselect: function(records, suppressEvent) {
         var me = this,
             selected = me.selected,
-            change = false,
             i = 0,
-            len, record;
-            
+            len, record,
+            attempted = 0,
+            accepted = 0;
+
         if (me.locked) {
-            return;
+            return false;
         }
 
         if (typeof records === "number") {
             records = [me.store.getAt(records)];
+        } else if (!Ext.isArray(records)) {
+            records = [records];
+        }
+
+        function commit () {
+            ++accepted;
+            selected.remove(record);
         }
 
-        records = !Ext.isArray(records) ? [records] : records;
         len = records.length;
+
         for (; i < len; i++) {
             record = records[i];
-            if (selected.remove(record)) {
+            if (me.isSelected(record)) {
                 if (me.lastSelected == record) {
                     me.lastSelected = selected.last();
                 }
-                me.onSelectChange(record, false, suppressEvent);
-                change = true;
+                ++attempted;
+                me.onSelectChange(record, false, suppressEvent, commit);
             }
         }
+
         // fire selchange if there was a change and there is no suppressEvent flag
-        me.maybeFireSelectionChange(change && !suppressEvent);
+        me.maybeFireSelectionChange(accepted > 0 && !suppressEvent);
+        return accepted === attempted;
     },
 
     doSingleSelect: function(record, suppressEvent) {
         var me = this,
+            changed = false,
             selected = me.selected;
-            
+
         if (me.locked) {
             return;
         }
@@ -365,22 +397,34 @@ Ext.define('Ext.selection.Model', {
         if (me.isSelected(record)) {
             return;
         }
-        if (selected.getCount() > 0) {
-            me.doDeselect(me.lastSelected, suppressEvent);
+
+        function commit () {
+            me.bulkChange = true;
+            if (selected.getCount() > 0 && me.doDeselect(me.lastSelected, suppressEvent) === false) {
+                delete me.bulkChange;
+                return false;
+            }
+            delete me.bulkChange;
+
+            selected.add(record);
+            me.lastSelected = record;
+            changed = true;
         }
-        selected.add(record);
-        me.lastSelected = record;
-        me.onSelectChange(record, true, suppressEvent);
-        if (!suppressEvent) {
-            me.setLastFocused(record);
+
+        me.onSelectChange(record, true, suppressEvent, commit);
+
+        if (changed) {
+            if (!suppressEvent) {
+                me.setLastFocused(record);
+            }
+            me.maybeFireSelectionChange(!suppressEvent);
         }
-        me.maybeFireSelectionChange(!suppressEvent);
     },
 
     /**
-     * @param {Ext.data.Model} record
-     * Set a record as the last focused record. This does NOT mean
+     * Sets a record as the last focused record. This does NOT mean
      * that the record has been selected.
+     * @param {Ext.data.Model} record
      */
     setLastFocused: function(record, supressFocus) {
         var me = this,
@@ -388,10 +432,10 @@ Ext.define('Ext.selection.Model', {
         me.lastFocused = record;
         me.onLastFocusChanged(recordBeforeLast, record, supressFocus);
     },
-    
+
     /**
      * Determines if this record is currently focused.
-     * @param Ext.data.Record record
+     * @param {Ext.data.Model} record
      */
     isFocused: function(record) {
         return record === this.getLastFocused();
@@ -413,27 +457,30 @@ Ext.define('Ext.selection.Model', {
     getLastSelected: function() {
         return this.lastSelected;
     },
-    
+
     getLastFocused: function() {
         return this.lastFocused;
     },
 
     /**
      * Returns an array of the currently selected records.
+     * @return {Ext.data.Model[]} The selected records
      */
     getSelection: function() {
         return this.selected.getRange();
     },
 
     /**
-     * Returns the current selectionMode. SINGLE, MULTI or SIMPLE.
+     * Returns the current selectionMode.
+     * @return {String} The selectionMode: 'SINGLE', 'MULTI' or 'SIMPLE'.
      */
     getSelectionMode: function() {
         return this.selectionMode;
     },
 
     /**
-     * Sets the current selectionMode. SINGLE, MULTI or SIMPLE.
+     * Sets the current selectionMode.
+     * @param {String} selModel 'SINGLE', 'MULTI' or 'SIMPLE'.
      */
     setSelectionMode: function(selMode) {
         selMode = selMode ? selMode.toUpperCase() : 'SINGLE';
@@ -451,26 +498,25 @@ Ext.define('Ext.selection.Model', {
     },
 
     /**
-     * Locks the current selection and disables any changes from
-     * happening to the selection.
-     * @param {Boolean} locked
+     * Locks the current selection and disables any changes from happening to the selection.
+     * @param {Boolean} locked  True to lock, false to unlock.
      */
     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
+     * Returns true if the specified row is selected.
+     * @param {Ext.data.Model/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.
+     * Returns true if there are any a selected records.
      * @return {Boolean}
      */
     hasSelection: function() {
@@ -504,7 +550,7 @@ Ext.define('Ext.selection.Model', {
         }
 
         me.clearSelections();
-        
+
         if (me.store.indexOf(lastFocused) !== -1) {
             // restore the last focus but supress restoring focus
             this.setLastFocused(lastFocused, true);
@@ -514,7 +560,7 @@ Ext.define('Ext.selection.Model', {
             // perform the selection again
             me.doSelect(toBeSelected, false, true);
         }
-        
+
         me.maybeFireSelectionChange(change);
     },
 
@@ -525,10 +571,9 @@ Ext.define('Ext.selection.Model', {
      */
     clearSelections: function() {
         // reset the entire selection to nothing
-        var me = this;
-        me.selected.clear();
-        me.lastSelected = null;
-        me.setLastFocused(null);
+        this.selected.clear();
+        this.lastSelected = null;
+        this.setLastFocused(null);
     },
 
     // when a record is added to a store
@@ -539,14 +584,9 @@ Ext.define('Ext.selection.Model', {
     // 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);
+        if (this.selected.getCount > 0) {
+            this.clearSelections();
+            this.maybeFireSelectionChange(true);
         }
     },
 
@@ -556,7 +596,7 @@ Ext.define('Ext.selection.Model', {
     onStoreRemove: function(store, record) {
         var me = this,
             selected = me.selected;
-            
+
         if (me.locked || !me.pruneRemoved) {
             return;
         }
@@ -572,6 +612,10 @@ Ext.define('Ext.selection.Model', {
         }
     },
 
+    /**
+     * Returns the count of selected records.
+     * @return {Number} The number of selected records
+     */
     getCount: function() {
         return this.selected.getCount();
     },
@@ -605,4 +649,4 @@ Ext.define('Ext.selection.Model', {
     bindComponent: function(cmp) {
 
     }
-});
\ No newline at end of file
+});