Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Multiple row selection using shift #369

Closed
adamclark64 opened this issue Aug 11, 2015 · 42 comments
Closed

Multiple row selection using shift #369

adamclark64 opened this issue Aug 11, 2015 · 42 comments

Comments

@adamclark64
Copy link

I am was wondering about multiple row selection using shift to select all rows between the first and second row click, instead of the command click for each individual row. Fairly new otherwise I would fork and attempt to add this in

@cp2587
Copy link

cp2587 commented Aug 13, 2015

+1

1 similar comment
@scotthovestadt
Copy link
Contributor

+1

@scotthovestadt
Copy link
Contributor

I'm using the following code to accomplish this, but plan to submit a pull request to hopefully add support in the core module. The code works as you'd expect - if you use just SHIFT, all other rows outside of the range will be deselected. If you use CMD+SHIFT (or CTRL on Windows), other rows outside of the range will not be deselected.

function rowClicked(params) {
  var self = this;

  // We have to wait otherwise it overrides our selection
  setTimeout(function waitForAngularGridToFinish() {
    // Select multiple rows when the shift key was pressed
    if(params.event.shiftKey && self.previousSelectedRowIndex !== undefined) {
      var smallerNumber = params.rowIndex < self.previousSelectedRowIndex ? params.rowIndex : self.previousSelectedRowIndex;
      var biggerNumber = params.rowIndex > self.previousSelectedRowIndex ? params.rowIndex : self.previousSelectedRowIndex;

      for(var rowIndexToSelect = smallerNumber; rowIndexToSelect <= biggerNumber; rowIndexToSelect++) {
        self.api.selectIndex(rowIndexToSelect, true, false);
      }
    }

    self.previousSelectedRowIndex = params.rowIndex;
  }, 0);
}

@scotthovestadt
Copy link
Contributor

@ceolter I think that if multiple row selection is enabled, the SHIFT key multiple selection should be enabled by default and it shouldn't be a parameter. Do you agree?

@ceolter
Copy link
Contributor

ceolter commented Aug 17, 2015

@scotthovestadt yes, shift parameter not neccessary.

@micabot
Copy link

micabot commented Sep 25, 2015

+1
Any news on this?

@ceolter
Copy link
Contributor

ceolter commented Sep 25, 2015

nope, on the backlog :(

@TFrascaroli
Copy link

Awmagawd I'd really love to have that feature on my grid. Definitely +1! (Any words on this @scotthovestadt ?)

@asfordmatt
Copy link

+1
This would be really nice, it is what most users expect with multi-select

@vladimir-ivanov
Copy link

+1

3 similar comments
@NisStrom
Copy link

+1

@jackyjieliu
Copy link

+1

@russell-miller
Copy link
Contributor

+1

@killyosaur
Copy link

+1. Any ETA on when you'll be looking into adding this?

@ceolter
Copy link
Contributor

ceolter commented Feb 8, 2016

no. doesn't meant not soon, just not ETA right now.

@anticommander
Copy link

I put together a pretty simple workaround given what's available in the existing API:

    $scope.gridOptions = {
      // Store app-specific parameters / functions here so they're accessible
      // within this object
      appSpecific:{},
      columnDefs:columnDefs,
      rowData:null,
      enableSorting:true,
      rowSelection:'multiple',
      suppressRowClickSelection:true,
      onRowClicked:function(row) {
        var lastSelectedRow = this.appSpecific.lastSelectedRow;
        var shiftKey = row.event.shiftKey,
          ctrlKey = row.event.ctrlKey;

        // If modifier keys aren't pressed then only select the row that was clicked
        if(!shiftKey && !ctrlKey) {
          this.api.deselectAll();
          this.api.selectNode(row.node, true);
        }
        // If modifier keys are used and there was a previously selected row
        else if(lastSelectedRow !== undefined) {
          // Select a block of rows
          if(shiftKey) {
            var startIndex, endIndex;
            // Get our start and end indexes correct
            if(row.rowIndex < lastSelectedRow.rowIndex) {
              startIndex = row.rowIndex;
              endIndex = lastSelectedRow.rowIndex;
            }
            else {
              startIndex = lastSelectedRow.rowIndex;
              endIndex = row.rowIndex;
            }
            // Select all the rows between the previously selected row and the 
            // newly clicked row
            for(var i = startIndex; i <= endIndex; i++) {
              this.api.selectIndex(i, true, true);
            }
          }
          // Select one more row
          if(ctrlKey) {
            this.api.selectIndex(row.rowIndex, true);
          }
        }
        // Store the recently clicked row for future use
        this.appSpecific.lastSelectedRow = row;
      },
      onBeforeSortChanged:function() {
        // Be sure to clear out the row selections on sort change since
        // row indexes will change
        this.api.deselectAll();
        this.appSpecific.lastSelectedRow = undefined;
      }
    };

@TFrascaroli
Copy link

That looks really nice! I'll try that out, because I'm desperate to have the functionality, and report back my findings. In the meantime, why don't you put it all together as a PR?

@TFrascaroli
Copy link

After a few trial and error I've got it working. It pains me to say that this has already been implemented by the author, but it's now under a commercial (paid) license. But, for those of you that want to continue using the free stuff, it's going to come in handy.

I've done some changes, the reasoning behind them is written in comments above the corresponding lines, starting with //CHANGED:

onRowClicked:function(row) {
                var lastSelectedRow = this.appSpecific.lastSelectedRow;
                var shiftKey = row.event.shiftKey,
                  ctrlKey = row.event.ctrlKey;

                // If modifier keys aren't pressed then only select the row that was clicked
                if(!shiftKey && !ctrlKey) {
                    //Removed the this.api.deselectAll(); since we can just tell the new selection to
                    //clear all previously selected rows (by passing a true in the second parameter)
                    //CHANGED: this.api.selectNode(row.node, true) to the following as per recomendation
                    //of the author (deprecation notice)
                    row.node.setSelected(true, true);
                }
                    // If modifier keys are used and there was a previously selected row
                else if(lastSelectedRow !== undefined) {
                    // Select a block of rows
                    if(shiftKey) {
                        var startIndex, endIndex;
                        // Get our start and end indexes correct
                        if(row.rowIndex < lastSelectedRow.rowIndex) {
                            startIndex = row.rowIndex;
                            endIndex = lastSelectedRow.rowIndex;
                        }
                        else {
                            startIndex = lastSelectedRow.rowIndex;
                            endIndex = row.rowIndex;
                        }
                        // Select all the rows between the previously selected row and the 
                        // newly clicked row
                        //CHANGED: `i <= endIndex` to the following so we skip the events callbacks (with the
                        //third argument `true`) in all but the last iteration
                        for(var i = startIndex; i < endIndex; i++) {
                            this.api.selectIndex(i, true, true);
                        }
                        //And we make the last one with a `false` so we get the event popped up.
                        this.api.selectIndex(i, true, false);
                    }
                    // Select one more row
                    if (ctrlKey) {
                        //CHANGED: this.api.selectNode(row.node, true) to the following as per recomendation
                        //of the author (deprecation notice)
                        row.node.setSelected(true);
                    }
                }
                // Store the recently clicked row for future use
                this.appSpecific.lastSelectedRow = row;
            },

@anticommander
Copy link

@TFrascaroli thanks for adding some fixes to my initial implementation. If you'd like to make a PR on my behalf I'd be more than happy to let you do so since you've improved the code.

@TFrascaroli
Copy link

Nah, I really don't care about "I did it, I should get the credit". Furthermore, I don't think it would pass Niall's approval since he clearly states in his project .md that

PR's on new features are not generally accepted.

And third I'm kinda bussy right now 😄

Glad to help 👍

@TFrascaroli
Copy link

Oh, and I found something else, change

if (ctrlKey) {
    row.node.setSelected(true);
}

to

if (ctrlKey) {
    if (this.api.gridOptionsWrapper.gridOptions.rowDeselection) {
        row.node.setSelected(!row.node.selected);
    } else {
        row.node.setSelected(true);
    }
}

So it emulates the deselection mecanism intended by the author (https://www.ag-grid.com/angular-grid-selection/index.php)

@ceolter
Copy link
Contributor

ceolter commented Mar 10, 2016

I don't think it would pass Niall's approval you make me sound like a difficult person to work with!!!

re free vs enterprise, all the selection stuff is in the free version.

@TFrascaroli
Copy link

I'm sorry if I made you feel that way, not what I intended. I just stated that because I thought you just would discard the hipothetical PR as "you're doing too much" (since you don't want the project to ultimatelly be a community project).

free vs enterprise, all the selection stuff is in the free version

Well, my bad.... but I don't know why I can't get it to work, neither in free nor in enterprise (I'm actually using this code here to make it work). I'll look into it.

@ceolter
Copy link
Contributor

ceolter commented Mar 11, 2016

@TFrascaroli lol - no offence taken at all my friend, you brought a smile to my face!! :)

@TFrascaroli
Copy link

@ceolter I'm back to spice this a little bit more.
In your docs you talk about selection, multiple selection and all that stuff (https://www.ag-grid.com/angular-grid-selection/index.php), but nowhere in that page it either implies or states that multiple range selection (the usual "CLICK + SHIFT + CLICK") works. And in fact, it doesn't (neither in your web nor in the many test cases I've put up). Therefore I', using my modified version of @anticommander 's code to make it work. No problems here.

The problem is that I now want to use enableRangeSelection (https://www.ag-grid.com/angular-grid-range-selection/index.php) and it doesn't work with my current setup. I think that, since I'm replacing the onRowClicked callback, it is absolutely foreseeable that it wouldn't work.

That brings up my question: will you implement click + shift + click selection in the future? The answer has been given, I know, just making sure. If the answer is still yes, will it be mutually exclusive with enableRangeSelection?
In the meantime I'll treat them as such (mutually exclusive) and disable/enable one or the other.

@ceolter
Copy link
Contributor

ceolter commented Mar 23, 2016

yes i still plan to implement multi row selection. i haven't thought about range selection - but your right, they don't work well together from a usability. i'll need to think it through when i implement as to how they will work together.

personally i would only use one of click selection and range selection.

@TFrascaroli
Copy link

Ok, I think I have a pretty good idea now of what should happen when you add all of this to the mix. I'll try to make a PR with some ideas and let you all know so you can all try it out. Obviously, you (@ceolter) don't have to merge the PR, but it can serve as a basis for your final implementation.

@ceolter
Copy link
Contributor

ceolter commented Mar 24, 2016

@TFrascaroli :)

this is deffo high on my list. if you raise a PR, it will be a kick in the bum for me to get it done!

@killyosaur
Copy link

While you are working on this, keep in mind that this should work when a user is only using check box selection as well as on row click. The above solution doesn't work with a setup that has row click selection disabled. (or it overrides the suppression of the row click selection, and considering the onRowClick event does not fire when a row is selected through a check box, this can be a bit of a pain.)

@TFrascaroli
Copy link

You're absolutely right, sir. I missed this completely. I won't be able to
work on this until the weekend, but I can tell you I'll be doing thing much
differently than this approach. I won't try to build a path (like in this
case) but rather build the functionality in the grid itself, and maybe
@ceolter can review and merge the PR. We'll see.... looks like we're all
bussy 😄
El 30/3/2016 22:43, "killyosaur" notifications@github.com escribió:

While you are working on this, keep in mind that this should work when a
user is only using check box selection as well as on row click. The above
solution doesn't work with a setup that has row click selection disabled.
(or it overrides the suppression of the row click selection, and
considering the onRowClick event does not fire when a row is selected
through a check box, this can be a bit of a pain.)


You are receiving this because you were mentioned.
Reply to this email directly or view it on GitHub
#369 (comment)

@clintb
Copy link

clintb commented Mar 31, 2016

@ceolter, @TFrascaroli; I hope this helps.

// Use this if you want enable users to have 
// "SELECT, (shift, control, or both), SELECT" 
// for range selection.

// This configuration assumes you only care about 
// selections via the checkboxes.

// This made it so I don't have to fool with onRowClicked at all.




// [BEGIN] capture key events
var _window = window;

if (!_window.keyvents) {

    _window.keyvents = {
        alt: false,
        shift: false,
        ctrl:false
    };

    // yes, I took the easy way out (jQuery) for key events. So what. :)
    $(window).keydown(function(ev) {
        _window.keyvents.alt = ev.altKey;
        _window.keyvents.shift = ev.shiftKey;
        _window.keyvents.ctrl = ev.ctrlKey;
    });

    $(window).keyup(function() {
        _window.keyvents.alt = false;
        _window.keyvents.shift = false;
        _window.keyvents.ctrl = false;
    });


}
// [END] capture key events


var f_handleRangeSelection = function(grid, event, individualSelectTask) {


    if (individualSelectTask === undefined) individualSelectTask = function() {};

    if (grid.selectingRange === true) {
        individualSelectTask();
        return;
    }

    var node = event.node;
    var select = node.selected;

    var lastSelectedRow = grid.appSpecific.lastSelectedRow;

    var selectRange = _window.keyvents.shift || _window.keyvents.ctrl;

    grid.selectingRange = selectRange;

    // If modifier keys aren't pressed then only select the row that was clicked
    if (selectRange === false) {
        individualSelectTask();
    }
    // If modifier keys are used and there was a previously selected row
    else if (lastSelectedRow !== undefined) {
        // Select a block of rows
        if (selectRange === true) {
            var startIndex, endIndex;
            // Get our start and end indexes correct
            if (node.childIndex < lastSelectedRow.childIndex) {
                startIndex = node.childIndex;
                endIndex = lastSelectedRow.childIndex;
            } else {
                startIndex = lastSelectedRow.childIndex;
                endIndex = node.childIndex;
            }
            // Select all the rows between the previously selected row and the 
            // newly clicked row
            for (var i = startIndex; i < endIndex; i++) {
                grid.api.selectIndex(i, select, true);
            }

            individualSelectTask();
        }
    }
    // Store the recently clicked row for future use
    grid.appSpecific.lastSelectedRow = node;
    grid.selectingRange = false;
};

var gridOptions = {
    selectingRange: false, // custom property for this purpose
    appSpecific: {}, // part of what anticommander originally contributed
    rowData: null,
    rowBuffer: 5,
    rowSelection: 'multiple',
    suppressRowClickSelection: true, // I do this because I only want selection (as well as range selection) to happen via the checkboxes
    onRowSelected: function (event) {

        // basically, wrap whatever you were going to do for 
        //'onRowSelected' with your handling of range selection
        f_handleRangeSelection(this, event, function() {

            // [BEGIN] whatever you were going to do...


            //event.node.data.selected = event.node.selected;
            //if (event.node.gridOptionsWrapper.gridOptions.slaveGrids[0]) {
            //  scope.selectRow(this.slaveGrids[0], event.node.id, event.node.selected);
            //  scope.setFilter(this.slaveGrids[0]);
            //}
            //scope.setFilter(event.node.gridOptionsWrapper.gridOptions);
            //updateRollingNumbers();


            // [END] whatever you were going to do...


        });


    },
    onBeforeSortChanged: function () {
        // Be sure to clear out the last selected row on sort change since
        // row indexes will change
        this.appSpecific.lastSelectedRow = undefined;
    },
    columnDefs: [
        { headerName: '', suppressMenu: true, checkboxSelection: true, maxWidth: 30, suppressSorting: true },
        { headerName: 'Make', field: 'make' },
        { headerName: 'Model', field: 'model' },
        { headerName: 'Year', field: 'year' }
    ]
};              

@clintb
Copy link

clintb commented Mar 31, 2016

Doh! It doesn't like using 'selectIndex'.

So, I modified the for loop in the range selection handler. Also, made it work in either direction (select or unselect).

// Use this if you want enable users to have 
// "SELECT, (shift, control, or both), SELECT" 
// for range selection.

// This configuration assumes you only care about 
// selections via the checkboxes.

// This made it so I don't have to fool with onRowClicked at all.




// [BEGIN] capture key events
var _window = window;

if (!_window.keyvents) {

    _window.keyvents = {
        alt: false,
        shift: false,
        ctrl:false
    };

    // yes, I took the easy way out (jQuery) for key events. So what.
    $(window).keydown(function(ev) {
        _window.keyvents.alt = ev.altKey;
        _window.keyvents.shift = ev.shiftKey;
        _window.keyvents.ctrl = ev.ctrlKey;
    });

    $(window).keyup(function() {
        _window.keyvents.alt = false;
        _window.keyvents.shift = false;
        _window.keyvents.ctrl = false;
    });


}
// [END] capture key events


var f_handleRangeSelection = function(grid, event, individualSelectTask) {


    if (individualSelectTask === undefined) individualSelectTask = function() {};

    if (grid.selectingRange === true) {
        individualSelectTask();
        return;
    }

    var node = event.node;
    var select = node.selected;

    var lastSelectedRow = grid.appSpecific.lastSelectedRow;

    var selectRange = _window.keyvents.shift || _window.keyvents.ctrl;

    grid.selectingRange = selectRange;

    // If modifier keys aren't pressed then only select the row that was clicked
    if (selectRange === false) {
        individualSelectTask();
    }
    // If modifier keys are used and there was a previously selected row
    else if (lastSelectedRow !== undefined) {
        // Select a block of rows
        if (selectRange === true) {
            var startIndex, endIndex;
            // Get our start and end indexes correct
            if (node.childIndex < lastSelectedRow.childIndex) {
                startIndex = node.childIndex;
                endIndex = lastSelectedRow.childIndex;
            } else {
                startIndex = lastSelectedRow.childIndex;
                endIndex = node.childIndex;
            }
            // Select all the rows between the previously selected row and the 
            // newly clicked row
            for (var i = startIndex; i < endIndex; i++) {
                grid.api.rowModel.getRow(i).setSelected(select, false, true); // [CHANGED]
            }

            individualSelectTask();
        }
    }
    // Store the recently clicked row for future use
    grid.appSpecific.lastSelectedRow = node;
    grid.selectingRange = false;
};

var gridOptions = {
    selectingRange: false, // custom property for this purpose
    appSpecific: {}, // part of what anticommander originally contributed
    rowData: null,
    rowBuffer: 5,
    rowSelection: 'multiple',
    suppressRowClickSelection: true, // I do this because I only want selection (as well as range selection) to happen via the checkboxes
    onRowSelected: function (event) {

        // basically, wrap whatever you were going to do for 
        //'onRowSelected' with your handling of range selection
        f_handleRangeSelection(this, event, function() {

            // [BEGIN] whatever you were going to do...


            //event.node.data.selected = event.node.selected;
            //if (event.node.gridOptionsWrapper.gridOptions.slaveGrids[0]) {
            //  scope.selectRow(this.slaveGrids[0], event.node.id, event.node.selected);
            //  scope.setFilter(this.slaveGrids[0]);
            //}
            //scope.setFilter(event.node.gridOptionsWrapper.gridOptions);
            //updateRollingNumbers();


            // [END] whatever you were going to do...


        });


    },
    onBeforeSortChanged: function () {
        // Be sure to clear out the last selected row on sort change since
        // row indexes will change
        this.appSpecific.lastSelectedRow = undefined;
    },
    columnDefs: [
        { headerName: '', suppressMenu: true, checkboxSelection: true, maxWidth: 30, suppressSorting: true },
        { headerName: 'Make', field: 'make' },
        { headerName: 'Model', field: 'model' },
        { headerName: 'Year', field: 'year' }
    ]
};              

@apala5
Copy link

apala5 commented Apr 13, 2016

To overcome the issue mentioned by @TFrascaroli. I just modified his code and add a one line change the ag-grid.js file.

I tested with multiple selection, single row selection, check box selection, check box multi selection and everything works good.

This solution will work whether suppressRowClickSelection is enabled or not.

Basically the row selection event will work as per the grid native functionality, Just registered the shift key as one of the multi selected key in ag-grid which will prevent deselection of a selected row.

//In ag-grid.js file modify the the below function to add the event.shiftKey in or condition.
RenderedRow.prototype.onRowClicked = function (event) {
      var multiSelectKeyPressed = event.ctrlKey || event.metaKey || event.shiftKey;
}

//In your js file add the onRowClicked evetn for the grid
onRowClicked: function (row) {               
                var lastSelectedRow = this.appSpecific.lastSelectedRow;
                var shiftKey = row.event.shiftKey,
                  ctrlKey = row.event.ctrlKey;

                // If modifier keys aren't pressed then only select the row that was clicked
                //if (!shiftKey && !ctrlKey) {
                //    this.api.deselectAll();
                //    this.api.selectNode(row.node, true);
                //}
                    // If modifier keys are used and there was a previously selected row
                 if (lastSelectedRow !== undefined) {
                    // Select a block of rows
                    if (shiftKey) {
                        var startIndex, endIndex;
                        // Get our start and end indexes correct
                        if (row.rowIndex < lastSelectedRow.rowIndex) {
                            startIndex = row.rowIndex;
                            endIndex = lastSelectedRow.rowIndex;
                        }
                        else {
                            startIndex = lastSelectedRow.rowIndex;
                            endIndex = row.rowIndex;
                        }
                        // Select all the rows between the previously selected row and the 
                        // newly clicked row
                        for (var i = startIndex; i < endIndex; i++) {
                            this.api.selectIndex(i, true, true);
                        }

                        //And we make the last one with a `false` so we get the event popped up.
                        this.api.selectIndex(i, true, false);
                    }
                    // Select one more row
                    //if (ctrlKey) {
                    //    this.api.selectIndex(row.rowIndex, true);
                    //}
                }
                // Store the recently clicked row for future use
                 this.appSpecific.lastSelectedRow = row;

            }

@ceolter
Copy link
Contributor

ceolter commented May 9, 2016

i have implemented this, will be in the next release.

@ceolter ceolter closed this as completed May 9, 2016
@mikeerickson
Copy link

@ceolter Not trying to put you on the spot, just curious you estimated ETA. This is one of the open items on my POC list. I was going to implement it myself, but if you are going to added it to agGrid core, then I would be better off waiting for you :-)

@ceolter
Copy link
Contributor

ceolter commented May 11, 2016

if not this week, then next week. that's my estimate.

@pccjamie
Copy link

Hi @ceolter - thanks for all the work. Just to clarify. Which release is this fix part of? Is it 427 which you just released a few days ago? https://github.com/ceolter/ag-grid/releases/tag/4.2.7

@ceolter
Copy link
Contributor

ceolter commented Jun 28, 2016

no this went in weeks ago. last weeks beta5 release also has it.

@Anruin
Copy link

Anruin commented Dec 12, 2016

Hi, @ceolter , should it work with virtual paging? It had worked for us when we've used in-memory row model, but then we switched to virtual paging mode with data retrieved from server. Now only Ctrl+Click works for multiple selection, Shift+Click doesn't. We're using 6.0.x at the moment. Can it be related to outdated version? Thanks.

@ceolter
Copy link
Contributor

ceolter commented Jan 5, 2017

only works with normal row model. this is because the viewport row model doesn't necessarily have all the rows loaded, so shift selection would only partially work if it was implemented.

@grovesNL
Copy link

Has anyone attempted to use this with dragging as well? Ideally Click then Drag could select ranges (the same as Shift-Click, then Click). When using with Ctrl it would combine the selections just like the behavior described in this issue.

Basically a similar idea to the enterprise feature for Range Selection but acting on full rows.

@TFrascaroli
Copy link

TFrascaroli commented Jan 12, 2017 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests