Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / selection / Model.js
diff --git a/src/selection/Model.js b/src/selection/Model.js
new file mode 100644 (file)
index 0000000..023eec0
--- /dev/null
@@ -0,0 +1,583 @@
+/**
+ * @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