AllenFang / react-bootstrap-table

A Bootstrap table built with React.js
https://allenfang.github.io/react-bootstrap-table/
MIT License
2.23k stars 782 forks source link

Support shift (keyboard) to select/unselect multiple rows #1574

Open dekelb opened 7 years ago

dekelb commented 7 years ago

It would be great if we could use the shift key (in the keyboard) to select multiple rows. For example - click on the "select" of row#1, press down the shift key, click on the "select" of row#5 -> result is that rows 1,2,3,4,5 are selected.

Same with unselect.

AllenFang commented 7 years ago

I'll consider to implement it but actually, it's a good idea to have some ways to trigger selection, I will also consider to implement on react-bootstrap-table2 thanks

dekelb commented 6 years ago

@AllenFang Was this implemented in react-bootstrap-table2?

skar404 commented 6 years ago

@AllenFang how can this be implemented?

BlaineBradbury commented 5 years ago

@AllenFang, I have found that this is a very important feature for wide adoption on list and select libraries. I find that the majority of the community libraries have skimped on this one. I've just implemented this functionality on my own dropdown library (in beta) successfully, and would be glad to contribute here, or share my own code as a starting place, or even just write out the detailed technical requirements if that will help.

This feature took lots of careful testing because handling keyup & keydown for shifts w/ arrows up/down or even changed mid-select is a lot of conditions while allowing for mouse click and shift-click to interoperate with the selections w/o issue. Add to that, managing the transitions across rows which were pre-selected before the shift-selection was begun, etc. I also found that the checkbox state boolean is not always the same as the row state boolean, depending on which elements are being used to construct the row and its wrapper(s). There are also some choices to consider as possible props for what happens when a selection is in place and a non-shift-selection occurs, which methods to clear all are supported or defaulted, etc. For larger datasets, performance becomes a consideration since certain points of selection state can only be understood or verified by searching the whole set. I also implemented a SelectionGroup dropdown which allowed for multiple list boxes to be configured. I wouldn't suggest that this was productive for most use cases or library weight, but it was very useful for me to learn that several issues that happen when multiple list boxes are deployed to keep state integrity between each, and to leave hooks for handling z-index layering, etc.

I am in process of migrating from react-bootstrap-table to -table2 on a smallish project where I must have shift-select, so I will need to go a different direction if I can't find a way to "hack it" in or else find the feature added.

Please let me know if I can help. thx

olegshepel commented 5 years ago

Dear, pls advice. I use react-bootstrap-table I can't select few checkboxes when holding shift. It's select only one.

How to set up to be able select checkboxes according click start and finish holding shift ? Thank you in advance ! Oleg

BlaineBradbury commented 5 years ago

@olegshepel It would take me some time to do a complete write-up on this, but I will do a quick brain dump here. Maybe that will be useful. The essential concept is to define and manage the user's "shift-selection" operation as a mini-lifecycle, and then commit their "selection-groups" to the underlying component's props and state only where and when needed to cause a re-render. You could do what I'm about to describe by creating your own component to wrap and implement the underlying table component along with your shift-select handling. This would keep things very tidy and away from your own app's implementation. I have done this, but only for specific applications which have too much other stuff going on to share here. For now, I'm going to explain the basics in a much more "hacky" manner, so that you might get a mental image of the concept requirements. From there, you could compose and implement much more deeply or elegantly as your case may need, or as your own application separation or quality concerns are to be met. (you might also be fine with the hack :)

Here are some basic ideas to consider:

  1. your onClick handler can detect shift via the event.shiftKey boolean (or else you could listen for shift with your own listener setup in componentDidMount and removed in componentWillUnMount
  2. the onClick handler can save the row position (which is also your data array position) that has taken the click event by adding it to your own "management" array. This can be done a couple ways, but to avoid state, I'll show it this way: this.myShiftArray[clickPositionValue]

You will have to determine your own requirements for what a shift+click would mean to your user or implementation, but probably some conditional logic in the handler like...:

  1. unless myShiftArray is empty, the first click on a row would pass event.shiftKey = false
  2. a second click event to the handler which still has event.shiftKey = false would mean the user simply clicked a different row and is not currently performing a shift+click operation. In this case, simply overwrite this.myShiftArray = [clickPositionValue]. This leaves the shift-selection-group's "start" value as the position last clicked before a shift+click event occurs.
  3. alternatively {if...else if...else, etc.}, when event.shiftKey = true, your handler would then check to see that this.myShiftArray.length === 1, so it can know that a prior click has occurred and the user is now performing a shift+click to complete the selection from that point (use min/max points to allow upward/downward selection). This is when you would perform a for loop to update your component's row selections to match the group of rows between the two points which the user has just "shift-selected".

This could mean a number of possibilities for you: a. setState() on your "selected" array which would pass down as props after render b. or maybe you do a dom.click() on each node to simulate the user's action (using something like event.target.parentElement.querySelectorAll("div"), etc.) to find those nodes c. or updating just the style or class value for those particular nodes if you are not passing selected items down to the component. You might find that passing the selections down to the underlying component could get very costly in render time if you are dealing with table's re-rendering hundreds of dom nodes. With react-bootstrap-table2 for instance, I let it handle the background style operation internally via its native row click, but I do not use the underlying onSelect or selected props, because the render time is way to long for a user who is in the middle of shifting and selecting, etc. With other libraries, you may have different options. d. your own invention...

Whether a, b, or c, You may want to skip rows which are already selected on the dom (could use another this.alreadySelected for this or else check the dom nodes for a "selection-row" class, etc. ) or are marked as nonSelectable by the underlying component (I use a this.state.nonSelectableIds array which gets updated as relevant).

NOTE: With b or c you will want/need to skip component updates via shouldComponentUpdate to avoid re-renders... again, it depends on the lib onto which you are bolting this functionality. You can set a boolean like this.skipShouldUpdateCase = true when you perform b or c. Then in shouldComponentUpdate, you will want to do something like if ( this.skipShouldUpdateCase ) { this.skipShouldUpdateCase = false; return false; }.

...here is only slightly better than pseudo-code to look at for some idea:

handleClick = ( event, dataRow, dataRowIndex ) => {
    const divNodes = event.target.parentElement.querySelectorAll("div")
    , isSelected = divNodes[dataRowIndex].classList.contains("selection-row")
    , isShift = event.shiftKey
    if ( this.myShiftArray === undefined ) this.myShiftArray = []
    if ( !isShift ) {
        if ( !isSelected ) {
            //normal select, overwrite this.myShiftArray
            this.myShiftArray = [dataRowIndex]
        } else {
            //DeSelect, remove from this.myShiftArray
            this.myShiftArray.length && this.myShiftArray.splice( 0, 1 )
        }
    } else if ( isShift ) {
        if ( !isSelected ) {
            if ( !this.myShiftArray.length ) {
                this.myShiftArray = [dataRowIndex]
            } else {
                this.myShiftArray.push( dataRowIndex )
                const rowPosArray = this.doShiftClickForLoop( divNodes, Math.min(...this.myShiftArray), Math.max(...this.myShiftArray) )
                this.refreshSelectionArray( divNodes, rowPosArray, true ) //push = true, splice = false, etc.
            }
        } else {
            this.myShiftArray.length && this.myShiftArray.splice( 0, 1 )
        }
    }
}

Armed with the above information, you would simply extend your event listening and handling to include keyUp events. The keyboard gets a bit more tricky as well, because you must handle selection while the user arrows up/down, and potentially across selected or nonSelectable rows, etc. This really isn't more difficult, just more tedious. It helps if you create background colors to distinguish between already-selected, shift-selection-about-to-be-selected and hovering-over-selected-item, etc.

Good luck, I hope this helps. If you get your prototype in place in a repo somewhere, I could certainly help a bit within reasonable time constraints.