1. #1
    Sencha Premium Member
    Join Date
    Apr 2008
    Posts
    18
    Vote Rating
    0
    kpopov is on a distinguished road

      0  

    Default Multiple Cells Selection Model

    Multiple Cells Selection Model


    I was wondering if anyone has any experience developing a custom grid cell selection model that would allow multiple cells to be selected at the same time. I need to be able to select several individual cells in an Editor Grid, possibly by dragging over the cells themselves.

    Any help would be appreciated.

  2. #2
    Sencha User harley.333's Avatar
    Join Date
    Mar 2007
    Posts
    286
    Vote Rating
    4
    harley.333 is on a distinguished road

      0  

    Default


    I got bored and wrote the following class. I mainly ripped off the Ext.grid.RowSelectionModel class, so this class also supports keyboard navigation.

    Hope it helps. I only wrote it because you mentioned it. Let me know if anything is broken so I can fix it here.
    Code:
    /**
     @class Ext.grid.CellSelectionModel
     * @extends Ext.grid.AbstractSelectionModel
     * Supports multiple selections and keyboard selection/navigation.
     * @constructor
     * @param {Object} config
     */
    Ext.grid.CellSelectionModel = function(config){
        Ext.apply(this, config);
        this.selections = [];
    
        this.last = false;
        this.lastActive = false;
    
        this.addEvents(
            /**
             * @event selectionchange
             * Fires when the selection changes
             * @param {SelectionModel} this
             * @param {Array} selections A multi-dimensional array containing the indices of all selected cells ([[0,0],[1,1]])
             */
            "selectionchange",
            /**
             * @event beforecellselect
             * Fires when a cell is being selected, return false to cancel.
             * @param {Ext.grid.CellSelectionModel} this
             * @param {Array} cellInfo An array of cell indices ([rowIndex, columnIndex])
             * @param {Boolean} keepExisting False if other selections will be cleared
             */
            "beforecellselect",
            /**
             * @event cellselect
             * Fires when a cell is selected.
             * @param {Ext.grid.CellSelectionModel} this
             * @param {Array} cellInfo An array of cell indices ([rowIndex, columnIndex])
             */
            "cellselect",
            /**
             * @event celldeselect
             * Fires when a cell is deselected.
             * @param {Ext.grid.CellSelectionModel} this
             * @param {Array} cellInfo An array of cell indices ([rowIndex, columnIndex])
             */
            "celldeselect"
        );
    
        Ext.grid.CellSelectionModel.superclass.constructor.call(this);
    };
    
    Ext.extend(Ext.grid.CellSelectionModel, Ext.grid.AbstractSelectionModel,  {
        /**
         * @cfg {Boolean} singleSelect
         * True to allow selection of only one cell at a time (defaults to false)
         */
        singleSelect : false,
    
        /**
         * @cfg {Boolean} moveEditorOnEnter
         * False to turn off moving the editor to the next cell when the enter key is pressed
         */
    
        // private
        initEvents : function(){
    
            if(!this.grid.enableDragDrop && !this.grid.enableDrag){
                this.grid.on("cellmousedown", this.handleMouseDown, this);
            }else{ // allow click to work like normal
                this.grid.on("cellclick", function(grid, rowIndex, columnIndex, e) {
                    if(e.button === 0 && !e.shiftKey && !e.ctrlKey) {
                        this.selectCell([rowIndex, columnIndex], false);
                        grid.view.focusCell(rowIndex, columnIndex);
                    }
                }, this);
            }
    
            this.rowNav = new Ext.KeyNav(this.grid.getGridEl(), {
                "up" : function(e) {
                    if (this.last == false) {
                        this.selectCell([0, 0]);
                    } else if (this.lastActive[0] == 0) {
                        return;
                    } else {
                        var row, col;
                        row = this.lastActive[0] - 1;
                        col = this.lastActive[1];
                        if (!e.shiftKey) {
                            this.selectCell([row, col]);
                        } else {
                            var last = this.last
                            this.selectRange(this.last, [row, col]);
                            this.last = last;
                        }
                        this.grid.getView().focusCell(row, col);
                        this.lastActive = [row, col];
                    }
                },
                "down" : function(e) {
                    if (this.last == false) {
                        this.selectCell([0, 0]);
                    } else if (this.lastActive[0] == this.grid.getStore().getCount() - 1) {
                        return;
                    } else {
                        var row, col;
                        row = this.lastActive[0] + 1;
                        col = this.lastActive[1];
                        if (!e.shiftKey) {
                            this.selectCell([row, col]);
                        } else {
                            var last = this.last
                            this.selectRange(this.last, [row, col]);
                            this.last = last;
                        }
                        this.grid.getView().focusCell(row, col);
                        this.lastActive = [row, col];
                    }
                },
                "left" : function(e) {
                    if (this.last == false) {
                        this.selectCell([0, 0]);
                    } else if (this.lastActive[1] == 0) {
                        return;
                    } else {
                        var row, col;
                        row = this.lastActive[0];
                        col = this.lastActive[1] - 1;
                        if (!e.shiftKey) {
                            this.selectCell([row, col]);
                        } else {
                            var last = this.last
                            this.selectRange(this.last, [row, col]);
                            this.last = last;
                        }
                        this.grid.getView().focusCell(row, col);
                        this.lastActive = [row, col];
                    }
                },
                "right" : function(e) {
                    if (this.last == false) {
                        this.selectCell([0, 0]);
                    } else if (this.lastActive[1] == this.grid.getColumnModel().getColumnCount() - 1) {
                        return;
                    } else {
                        var row, col;
                        row = this.lastActive[0];
                        col = this.lastActive[1] + 1;
                        if (!e.shiftKey) {
                            this.selectCell([row, col]);
                        } else {
                            var last = this.last
                            this.selectRange(this.last, [row, col]);
                            this.last = last;
                        }
                        this.grid.getView().focusCell(row, col);
                        this.lastActive = [row, col];
                    }
                },
                scope: this
            });
    
            var view = this.grid.view;
            view.on("refresh", this.onRefresh, this);
            view.on("rowremoved", this.onRemove, this);
        },
    
        // private
        onRefresh : function(){
            this.clearSelections();
        },
    
        // private
        onRemove : function(v, index, r){
        	this.deselectRange([index, 0], [index, this.grid.getColumnModel().getColumnCount()]);
        },
    
        /**
         * Gets the number of selected cells.
         * @return {Number}
         */
        getCount : function(){
            return this.selections.length;
        },
    
        /**
         * Selects the cell to the right of the last selected cell.
         * @param {Boolean} keepExisting (optional) True to keep existing selections
         * @return {Boolean} True if selection is successful, else false
         */
        selectRight : function(keepExisting){
            if(this.hasNext()){
                var row, col = this.last[1];
                if (col == this.grid.getColumnModel().getColumnCount() - 1) {
                    row = this.last[0] + 1;
                    col = 0;
                } else {
                    row = this.last[0];
                    col += 1;
                }
    
                this.selectCell([row, col], keepExisting);
                this.grid.getView().focusCell(this.last[0], this.last[1]);
                return true;
            }
            return false;
        },
    
        /**
         * Selects the cell underneath the last selected cell.
         * @param {Boolean} keepExisting (optional) True to keep existing selections
         * @return {Boolean} True if selection is successful, else false
         */
        selectDown : function(keepExisting) {
            var r, cols = this.grid.getColumnModel().getColumnCount();
            for (var i = 0; i < cols; i++) {
                r = this.selectRight(keepExisting);
                if (!r) break;
            }
            return r;
        },
    
        /**
         * Selects the cell above the last selected cell.
         * @param {Boolean} keepExisting (optional) True to keep existing selections
         * @return {Boolean} True if selection is successful, else false
         */
        selectUp : function(keepExisting) {
            var r, cols = this.grid.getColumnModel().getColumnCount();
            for (var i = 0; i < cols; i++) {
                r = this.selectLeft(keepExisting);
                if (!r) break;
            }
            return r;
        },
    
        /**
         * Selects the cell to the left of the last selected cell.
         * @param {Boolean} keepExisting (optional) True to keep existing selections
         * @return {Boolean} True if selection is successful, else false
         */
        selectLeft : function(keepExisting) {
            if (this.hasPrevious()) {
                var row, col = this.last[1];
                if (col == 0) {
                    row = this.last[0] - 1;
                    col = this.grid.getColumnModel().getColumnCount() - 1;
                } else {
                    row = this.last[0];
                    col -= 1;
                }
                
                this.selectCell([row, col], keepExisting);
                this.grid.getView().focusCell(this.last[0], this.last[1]);
                return true;
            }
            return false;
        },
    
        /**
         * Returns true if there is a next cell to select
         * @return {Boolean}
         */
        hasNext : function() {
            return this.last !== false && ((this.last[0] + 1) < this.grid.store.getCount() || (this.last[1] + 1) < this.grid.getColumnModel().getColumnCount());
        },
    
        /**
         * Returns true if there is a previous cell to select
         * @return {Boolean}
         */
        hasPrevious : function(){
            return this.last !== false && (this.last[0] != 0 || this.last[1] != 0);
        },
    
    
        /**
         * Returns the selected cell indices
         * @return {Array} Array of cell indices ([rowIndex, columnIndex])
         */
        getSelections : function() {
            return [].concat(this.selections);
        },
    
        /**
         * Returns the first selected cell index.
         * @return {Array} An array containing the row and column indexes of the first selected cell, or null if none selected.
         */
        getSelectedCell : function() {
            return this.selections.length > 0 ? [].concat(this.selections[0]) : null;
        },
    
        /**
         * Calls the passed function with each selection. If the function returns false, iteration is
         * stopped and this function returns false. Otherwise it returns true.
         * @param {Function} fn
         * @param {Object} scope (optional)
         * @return {Boolean} true if all selections were iterated
         */
        each : function(fn, scope) {
            var s = this.getSelections();
            for (var i = 0, len = s.length; i < len; i++) {
                if (fn.call(scope || this, s[i], i) === false) {
                    return false;
                }
            }
            return true;
        },
    
        /**
         * Clears all selections.
         */
        clearSelections : function(){
            if(this.locked) return;
            for (var i = this.selections.length - 1; i >= 0 ; i--) {
                this.deselectCell(this.selections[i]);
            }
            this.selections = [];
            this.last = false;
        },
    
    
        /**
         * Selects all cells.
         */
        selectAll : function(){
            if(this.locked) return;
            this.selections = [];
            var row, col;
            var rowCount = this.grid.GetStore().getCount()
            var colCount = this.grid.GetColumnModel().getColumnCount()
            for(row = 0; row < rowCount; row++){
                for(col = 0; col < colCount; col++){
                    this.selectCell([row, cell], true);
                }
            }
        },
    
        /**
         * Returns True if there is a selection.
         * @return {Boolean}
         */
        hasSelection : function(){
            return this.selections.length > 0;
        },
    
        /**
         * Returns True if the specified cell is selected.
         * @param {Array/Record} record The cell-index ([rowIndex, columnIndex]) to check
         * @return {Boolean}
         */
        isSelected : function(index){
            var s = this.selections;
            for (var i = 0; i < s.length; i++) {
                if (s[i][0] == index[0] && s[i][1] == index[1]) {
                    return true;
                }
            }
            return false;
        },
    
        // private
        handleMouseDown : function(g, rowIndex, columnIndex, e){
            if(e.button !== 0 || this.isLocked()){
                return;
            };
            var view = this.grid.getView();
            if(e.shiftKey && this.last !== false){
                var last = this.last;
                this.selectRange(last, [rowIndex, columnIndex], e.ctrlKey);
                this.last = last; // reset the last
                view.focusCell(rowIndex, columnIndex);
            }else{
                var isSelected = this.isSelected([rowIndex, columnIndex]);
                if(e.ctrlKey && isSelected){
                    this.deselectCell([rowIndex, columnIndex]);
                }else if(!isSelected || this.getCount() > 1){
                    this.selectCell([rowIndex, columnIndex], e.ctrlKey || e.shiftKey);
                    view.focusCell(rowIndex, columnIndex);
                }
            }
        },
    
        /**
         * Selects multiple cells.
         * @param {Array} cells Array of the indices ([rowIndex, columnIndex]) of the cells to select
         * @param {Boolean} keepExisting (optional) True to keep existing selections (defaults to false)
         */
        selectCells : function(cells, keepExisting){
            if(!keepExisting){
                this.clearSelections();
            }
            for(var i = 0, len = cells.length; i < len; i++){
                this.selectCell(cells[i], true);
            }
        },
    
        /**
         * Selects a range of cells. All cells in between startCell and endCell are also selected.
         * @param {Array} startCell The index of the first cell ([rowIndex, columnIndex]) in the range
         * @param {Array} endCell The index of the last cell ([rowIndex, columnIndex]) in the range
         * @param {Boolean} keepExisting (optional) True to retain existing selections
         */
        selectRange : function(startCell, endCell, keepExisting){
            if(this.locked) return;
            if(!keepExisting){
                this.clearSelections();
            }
            var row, col, colCount;
            var startRow = startCell[0];
            var startCol = startCell[1];
            var endRow = endCell[0];
            var endCol = endCell[1];
            if (endRow < startRow) {
                // flip 'em
                row = endRow;
                endRow = startRow;
                startRow = row;
            }
            if (endCol < startCol) {
                // flip 'em
                col = endCol;
                endCol = startCol;
                startCol = col;
            }
            for (row = startRow; row <= endRow; row++) {
                for (col = startCol; col <= endCol; col++) {
                    this.selectCell([row, col], true);
                }
            }
        },
    
        /**
         * Deselects a range of cells. All cells in between startCell and endCell are also deselected.
         * @param {Array} startCell The index of the first cell ([rowIndex, columnIndex]) in the range
         * @param {Array} endCell The index of the last cell ([rowIndex, columnIndex]) in the range
         */
        deselectRange : function(startCell, endCell, preventViewNotify) {
            if(this.locked) return;
            var row, col, colCount;
            var startRow = startCell[0];
            var startCol = startCell[1];
            var endRow = endCell[0];
            var endCol = endCell[1];
            if (endRow < startRow) {
                // flip 'em
                row = endRow;
                endRow = startRow;
                startRow = row;
            }
            if (endCol < startCol) {
                // flip 'em
                col = endCol;
                endCol = startCol;
                startCol = col;
            }
            for (row = startRow; row <= endRow; row++) {
                for (col = startCol; col <= endCol; col++) {
                    this.deselectCell([row, col], preventViewNotify);
                }
            }
        },
    
        /**
         * Selects a cell.
         * @param {Array} cell The index of the cell ([rowIndex, columnIndex]) to select
         * @param {Boolean} keepExisting (optional) True to keep existing selections
         */
        selectCell : function(index, keepExisting, preventViewNotify){
            if (this.locked) return;
            if (this.isSelected(index)) return;
            var row = index[0];
            var col = index[1];
            if (row < 0 || row >= this.grid.store.getCount()) return;
            if (col < 0 || col >= this.grid.getColumnModel().getColumnCount()) return;
    
            if (this.fireEvent("beforecellselect", this, index, keepExisting) !== false) {
                if (!keepExisting || this.singleSelect) {
                    this.clearSelections();
                }
                this.selections.push(index);
                this.last = this.lastActive = index;
                if(!preventViewNotify) {
                    this.grid.getView().onCellSelect(row, col);
                }
                this.fireEvent("cellselect", this, index);
                this.fireEvent("selectionchange", this, [].concat(this.selections));
            }
        },
    
        /**
         * Deselects a cell.
         * @param {Array} cell The index of the cell ([rowIndex, columnIndex]) to deselect
         */
        deselectCell : function(index, preventViewNotify){
            if (this.locked) return;
            if (this.last[0] == index[0] && this.last[1] == index[1]) {
                this.last = false;
            }
            if (this.lastActive[0] == index[0] && this.lastActive[1] == index[1]) {
                this.lastActive = false;
            }
    
            var s = this.selections;
            for (var i = 0; i < s.length; i++) {
                if (s[i][0] == index[0] && s[i][1] == index[1]) {
                    this.selections.remove(s[i]);
                    if (!preventViewNotify) {
                        this.grid.getView().onCellDeselect(index[0], index[1]);
                    }
                    this.fireEvent("celldeselect", this, index);
                    this.fireEvent("selectionchange", [].concat(this.selections));
                    return;
                }
            }
    
        },
    
        // private
        acceptsNav : function(row, col, cm){
            return !cm.isHidden(col) && cm.isCellEditable(col, row);
        },
    
        // private
        onEditorKey : function(field, e){
            var k = e.getKey(), newCell, g = this.grid, ed = g.activeEditor;
            var shift = e.shiftKey;
            if(k == e.TAB){
                e.stopEvent();
                ed.completeEdit();
                if(shift){
                    newCell = g.walkCells(ed.row, ed.col-1, -1, this.acceptsNav, this);
                }else{
                    newCell = g.walkCells(ed.row, ed.col+1, 1, this.acceptsNav, this);
                }
            }else if(k == e.ENTER){
                e.stopEvent();
                ed.completeEdit();
                if(this.moveEditorOnEnter !== false){
                    if(shift){
                        newCell = g.walkCells(ed.row - 1, ed.col, -1, this.acceptsNav, this);
                    }else{
                        newCell = g.walkCells(ed.row + 1, ed.col, 1, this.acceptsNav, this);
                    }
                }
            }else if(k == e.ESC){
                ed.cancelEdit();
            }
            if(newCell){
                g.startEditing(newCell[0], newCell[1]);
            }
        }
    });
    And here's a sample to show off the usage:
    Code:
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
        <head>
            <title>Ext.grid.CellSelectionModel Sample</title>
            <meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
            <meta name="robots" content="noindex,nofollow" />
            <link type="text/css" rel="stylesheet" href="css/ext-all.css" />
        </head>
        <body>
            <form enctype="multipart/form-data">
                <script type="text/javascript" src="js/ext-base.js"></script>
                <script type="text/javascript" src="js/ext-all.js"></script>
                <script type="text/javascript" src="js/Ext.grid.CellSelectionModel.js"></script>
                <script type="text/javascript">
    TheApp = {
        init: function() {
            Ext.BLANK_IMAGE_URL = "images/default/s.gif";
            Ext.QuickTips.init();
    
    
            var data = [];
            var MAX = 10;
            for (var x = 0; x < MAX; x++) {
                var tmp = [];
                for (var y = 0; y < MAX; y++) {
                    tmp.push(x.toString() + ", " + y.toString());
                }
                data.push(tmp);
            }
            var fields = [];
            for (var x = 0; x < MAX; x++) {
                var name = "col" + x.toString();
                fields.push({name: name, mapping: x, type: "string"});
            }
            var cols = [];
            for (var x = 0; x < MAX; x++) {
                var name = "col" + x.toString();
                cols.push({dataIndex: name, header: name});
            }
    
            var store = new Ext.data.Store({
                proxy: new Ext.data.MemoryProxy(data),
                reader: new Ext.data.ArrayReader(
                    {id: 0},
                    fields
                ),
                sortInfo: {field: "col0"}
            });
            store.load();
            var layout = new Ext.Viewport({
                layout: "fit",
                items: [{
                    xtype: "editorgrid",
                    selModel: new Ext.grid.CellSelectionModel(),
                    store: store,
                    columns: cols
                }]
            });
        }
    };
    Ext.onReady(TheApp.init, TheApp, true);
                </script>
            </form>
        </body>
    </html>

  3. #3
    Ext User
    Join Date
    Jun 2009
    Posts
    15
    Vote Rating
    0
    oshannon is on a distinguished road

      0  

    Default


    Cool it works! I'm going to have to modify it a bit because I've created a calendar and the shift selecting will have to be different (selecting square blocks in a calendar doesn't make sense), but this will definitely help!!

  4. #4
    Ext User
    Join Date
    Sep 2008
    Location
    colombia
    Posts
    13
    Vote Rating
    0
    israeldelahoz is on a distinguished road

      0  

    Question


    i have my grid on a tab panel,but when i close the tab i got this error when i use yor code for selection model

    Code:
    this.getRow(row) is undefined
    here

    Code:
     getCell : function(row, col){
     return this.getRow(row).getElementsByTagName('td')[col];
    },
    in Ext.grid.GridView

  5. #5
    Ext User alebar's Avatar
    Join Date
    Apr 2010
    Posts
    21
    Vote Rating
    0
    alebar is on a distinguished road

      0  

    Default cell focus after an action is performed with them

    cell focus after an action is performed with them


    Hi,
    I'm using this extension and it works very well. The point is that if I perform an action (like single cell swapping) on a given cell (for example cell [3,1]), I lose the focus on that cell and the cell [0,0] is focused.
    Is it possible to hold the focus on a given cell after an action is performed on it?

    I thought to use myGrid.getSelectionModel().fireEvent('cellselect', arrayofcoordinates) at the bottom of my function to focus the previous selected cell but it doesn't seems to work.

    Any suggestion?

  6. #6
    Ext User
    Join Date
    Apr 2010
    Posts
    8
    Vote Rating
    0
    darklow is on a distinguished road

      0  

    Default


    I created quick version example by using this CellSelectionModel together with DragSelector v3
    More here: http://www.sencha.com/forum/showthre...193#post519193

  7. #7
    Sencha User
    Join Date
    Mar 2007
    Posts
    38
    Vote Rating
    0
    tbarstow is on a distinguished road

      0  

    Default


    This is great code and still works as of Ext 3.x. Might be obsolete as of 4.0 but I still have 3.x projects that benefit from this!

    I have made some improvements -- there were some keyboard navigation bugs in the original implementation.

    Updated code with version history here:

    https://gist.github.com/959093/165a4...749af6d200ca7b

  8. #8
    Sencha User
    Join Date
    Oct 2007
    Location
    Berlin, Germany
    Posts
    889
    Vote Rating
    9
    wm003 will become famous soon enough

      0  

    Default


    I know this thread is 3 years old but i also have massive ext 3 projects and this was a huge timesaver. Thanks a lot!