97

In GMail, the user can click on one checkbox in the email list, hold down the Shift key, and select a second checkbox. The JavaScript will then select/unselect the checkboxes that are between the two checboxes.

I am curious as to how this is done? Is this JQuery or some basic (or complex) JavaScript?

Ascalonian
  • 14,409
  • 18
  • 71
  • 103

15 Answers15

209

I wrote a self-contained demo that uses jquery:

$(document).ready(function() {
    var $chkboxes = $('.chkbox');
    var lastChecked = null;

    $chkboxes.click(function(e) {
        if (!lastChecked) {
            lastChecked = this;
            return;
        }

        if (e.shiftKey) {
            var start = $chkboxes.index(this);
            var end = $chkboxes.index(lastChecked);

            $chkboxes.slice(Math.min(start,end), Math.max(start,end)+ 1).prop('checked', lastChecked.checked);
        }

        lastChecked = this;
    });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
<head>
</head>
<body>
    <input type="checkbox" id="id_chk1" class="chkbox" value="1" />Check 1<br/>
    <input type="checkbox" id="id_chk2" class="chkbox" value="2" />Check 2<br/>
    <input type="checkbox" id="id_chk3" class="chkbox" value="3" />Check 3<br/>
    <input type="checkbox" id="id_chk4" class="chkbox" value="4" />Check 4<br/>
    <input type="checkbox" id="id_chk5" class="chkbox" value="5" />Check 5<br/>
    <input type="checkbox" id="id_chk6" class="chkbox" value="6" />Check 6<br/>
    <input type="checkbox" id="id_chk7" class="chkbox" value="7" />Check 7<br/>
</body>
</html>
BC.
  • 24,298
  • 12
  • 47
  • 62
  • 8
    You can call slice instead of using the for loop. It would look like this: "$('.chkbox').slice(min..., max... + 1).attr('checked', lastChecked.checked)" – Matthew Crumley Mar 18 '09 at 23:16
  • 11
    Answer is lame without abstracted jquery plugin. So here you go https://gist.github.com/3784055 – Andy Ray Sep 25 '12 at 19:54
  • 3
    Doesn't seem to work for doing a shift-click multiple times after un-checking some boxes without shift-click. http://jsfiddle.net/5fG5b/ – Greg Pettit Feb 18 '14 at 15:04
  • 3
    That's because it should be `.prop('checked'`, not `.attr('checked'`. jsFiddle: http://jsfiddle.net/dn4jv9a5/ – caitlin Jan 07 '15 at 19:04
  • 4
    @schnauss thank you, you are right and I've updated the answer. In my defense, the original answer was written before prop() was available – BC. Jan 08 '15 at 20:16
  • I would also like to UNSELECT multiple boxes at once, preferrably also with shift-click. How to change the example? – Stefan Reich May 15 '17 at 13:04
37

This is done through fairly simple javascript.

They keep track of the id of the last checked box and when when another checkbox is checked they use the shiftKey event attribute to see if shift was held while clicking the checkbox. If so they set the checked property of each checkbox in between the two to true.

To determine when a box is checked they probably use an onclick event on the checkboxes

Ben S
  • 68,394
  • 30
  • 171
  • 212
  • 3
    If you wanna, you can use this references from Mozilla Developer Network: [shiftKey event attribute](https://developer.mozilla.org/en-US/docs/DOM/event.shiftKey), [Input element properties](https://developer.mozilla.org/en-US/docs/DOM/HTMLInputElement), [onclick](https://developer.mozilla.org/en-US/docs/DOM/element.onclick). – PhoneixS Nov 30 '12 at 12:12
15

It seems like every answer I can find online is completely dependent on jQuery for this. JQuery adds very little functionality. Here's a quick version that doesn't require any frameworks:

function allow_group_select_checkboxes(checkbox_wrapper_id){
    var lastChecked = null;
    var checkboxes = document.querySelectorAll('#'+checkbox_wrapper_id+' input[type="checkbox"]');

    //I'm attaching an index attribute because it's easy, but you could do this other ways...
    for (var i=0;i<checkboxes.length;i++){
        checkboxes[i].setAttribute('data-index',i);
    }

    for (var i=0;i<checkboxes.length;i++){
        checkboxes[i].addEventListener("click",function(e){

            if(lastChecked && e.shiftKey) {
                var i = parseInt(lastChecked.getAttribute('data-index'));
                var j = parseInt(this.getAttribute('data-index'));
                var check_or_uncheck = this.checked;

                var low = i; var high=j;
                if (i>j){
                    var low = j; var high=i; 
                }

                for(var c=0;c<checkboxes.length;c++){
                    if (low <= c && c <=high){
                        checkboxes[c].checked = check_or_uncheck;
                    }   
                }
            } 
            lastChecked = this;
        });
    }
}

And then initialize it whenever you need it:

allow_group_select_checkboxes('[id of a wrapper that contains the checkboxes]')
sebas
  • 1,283
  • 1
  • 12
  • 16
Ben D
  • 14,321
  • 3
  • 45
  • 59
14

Recently, I wrote a jQuery plugin that provide that feature and more.

After including the plugin you just need to initialize the context of checkboxes with the following code snippet:

$('#table4').checkboxes({ range: true });

Here is the link to the documentation, demo & download: http://rmariuzzo.github.io/checkboxes.js/

Rubens Mariuzzo
  • 28,358
  • 27
  • 121
  • 148
3

Got this solution from http://abcoder.com/javascript/jquery/simple-check-uncheck-all-jquery-function/ (now dead):

JavaScript and HTML code

var NUM_BOXES = 10;

// last checkbox the user clicked
var last = -1;

function check(event) {
  // in IE, the event object is a property of the window object
  // in Mozilla, event object is passed to event handlers as a parameter
  if (!event) { event = window.event }
  var num = parseInt(/box\[(\d+)\]/.exec(this.name)[1]);
  if (event.shiftKey && last != -1) {
     var di = num > last ? 1 : -1;
     for (var i = last; i != num; i += di) {
        document.forms.boxes['box[' + i + ']'].checked = true;
     }
  }
  last = num;
}

function init() {
  for (var i = 0; i < NUM_BOXES; i++) {
    document.forms.boxes['box[' + i + ']'].onclick = check;
  }
}
<body onload="init()">
    <form name="boxes">
    <input name="box[0]" type="checkbox">
    <input name="box[1]" type="checkbox">
    <input name="box[2]" type="checkbox">
    <input name="box[3]" type="checkbox">
    <input name="box[4]" type="checkbox">
    <input name="box[5]" type="checkbox">
    <input name="box[6]" type="checkbox">
    <input name="box[7]" type="checkbox">
    <input name="box[8]" type="checkbox">
    <input name="box[9]" type="checkbox">
    </form>
</body>
jackotonye
  • 3,537
  • 23
  • 31
Adnan
  • 2,001
  • 2
  • 26
  • 28
3

Well, the post is quite old but here is a solution I've just come across: jQuery Field Plug-In

Mike
  • 538
  • 3
  • 14
3

I took the jQuery version from @BC. and transformed it into an ES6 version, since the code is actually pretty elegantly solving the problem, in case anyone still stumbles across this...

function enableGroupSelection( selector ) {
  let lastChecked = null;
  const checkboxes = Array.from( document.querySelectorAll( selector ) );

  checkboxes.forEach( checkbox => checkbox.addEventListener( 'click', event => {
    if ( !lastChecked ) {
      lastChecked = checkbox;

      return;
    }

    if ( event.shiftKey ) {
      const start = checkboxes.indexOf( checkbox );
      const end   = checkboxes.indexOf( lastChecked );

      checkboxes
        .slice( Math.min( start, end ), Math.max( start, end ) + 1 )
        .forEach( checkbox => checkbox.checked = lastChecked.checked );
    }

    lastChecked = checkbox;
  } ) );
}
Moritz Friedrich
  • 1,371
  • 20
  • 38
2

Inspired by the fine answers provided, here's a plain JavaScript version using Array.prototype to coerce nodelists to use array functions, rather than for loops.

(function () { // encapsulating variables with IIFE
  var lastcheck = null // no checkboxes clicked yet

  // get desired checkboxes
  var checkboxes = document.querySelectorAll('div.itemslist input[type=checkbox]')

  // loop over checkboxes to add event listener
  Array.prototype.forEach.call(checkboxes, function (cbx, idx) {
    cbx.addEventListener('click', function (evt) {

      // test for shift key, not first checkbox, and not same checkbox
      if ( evt.shiftKey && null !== lastcheck && idx !== lastcheck ) {

        // get range of checks between last-checkbox and shift-checkbox
        // Math.min/max does our sorting for us
        Array.prototype.slice.call(checkboxes, Math.min(lastcheck, idx), Math.max(lastcheck, idx))
          // and loop over each
          .forEach(function (ccbx) {
            ccbx.checked = true
        })
      }
      lastcheck = idx // set this checkbox as last-checked for later
    })
  })
}())
<div class="itemslist">
  <input type="checkbox" name="one"   value="1">
  <input type="checkbox" name="two"   value="2">
  <input type="checkbox" name="three" value="3">
  <input type="checkbox" name="four"  value="4">
  <input type="checkbox" name="five"  value="5">
</div>
bloodyKnuckles
  • 11,551
  • 3
  • 29
  • 37
2

I realy liked gyo's example and added some code so it works on all checkboxes with the same name.

I also added a MutationObserver so events are also handled on newly added checkboxes.

$(document).ready(function() {
    var previouslyClicked = {};

    var rangeEventHandler = function(event) {
        if (event.shiftKey && previouslyClicked[this.name] && this != previouslyClicked[this.name]) {
            var $checkboxes = $('input[type=checkbox][name='+this.name+']').filter(':visible');
            var start = $checkboxes.index( this );
            var end = $checkboxes.index( previouslyClicked[this.name] );
//              console.log('range', start, end, this, previouslyClicked[this.name]);
            $checkboxes.slice(Math.min(start,end), Math.max(start,end)+ 1).prop('checked', previouslyClicked[this.name].checked);
        } else {
            previouslyClicked[this.name] = this;
        }
    };

    if ("MutationObserver" in window) { // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver/MutationObserver to refresh on new checkboxes
        var mutationCallback = function(mutationList, observer) {
            mutationList.forEach((mutation) => {
                mutation.addedNodes.forEach((node) => {
                    if (node.nodeName == 'INPUT' && node.type == 'checkbox') {
                        $(node).on('click.selectRange', rangeEventHandler);
                    }
                });
            });
        };

        var observer = new MutationObserver(mutationCallback);
        observer.observe(document, {
            childList: true,
            attributes: false,  // since name is dynamically read
            subtree: true
        });
    }

    $('input[type=checkbox][name]').on('click.selectRange', rangeEventHandler);
});
<html>
<head>
</head>
<body>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
  <div>
    First:
    <input type="checkbox" name="first">
    <input type="checkbox" name="first">
    <input type="checkbox" name="first">
    <input type="checkbox" name="first">
    <input type="checkbox" name="first">
  </div>
  <div>
    Second:
    <input type="checkbox" name="second">
    <input type="checkbox" name="second">
    <input type="checkbox" name="second">
    <input type="checkbox" name="second">
    <input type="checkbox" name="second">
  </div>
</body>
</html>
Gerben Versluis
  • 547
  • 6
  • 7
1
  • Found the better solution it works for both select and deselects checkboxes.

  • Uses a core javascript & Jquery.

$(document).ready(function() {
    var $chkboxes = $('.chkbox');
    var lastChecked = null;

    $chkboxes.click(function(e) {
        if(!lastChecked) {
            lastChecked = this;
            return;
        }

        if(e.shiftKey) {
            var start = $chkboxes.index(this);
            var end = $chkboxes.index(lastChecked);

            $chkboxes.slice(Math.min(start,end), Math.max(start,end)+ 1).prop('checked', e.target.checked);

        }

        lastChecked = this;
    });
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html>
    <head>
    </head>
    <body>
        <input type="checkbox" id="id_chk1" class="chkbox" value="1" />Check 1<br/>
        <input type="checkbox" id="id_chk2" class="chkbox" value="2" />Check 2<br/>
        <input type="checkbox" id="id_chk3" class="chkbox" value="3" />Check 3<br/>
        <input type="checkbox" id="id_chk4" class="chkbox" value="4" />Check 4<br/>
        <input type="checkbox" id="id_chk5" class="chkbox" value="5" />Check 5<br/>
        <input type="checkbox" id="id_chk6" class="chkbox" value="6" />Check 6<br/>
        <input type="checkbox" id="id_chk7" class="chkbox" value="7" />Check 7<br/>
    </body>
</html>
1

A lot of good answers to a very old question, however none of them take into consideration that most people click on the checkbox label when selecting an option, not the checkbox itself. Below are a couple of listeners that allow you to click on a checkbox or its accompanying label to select a range.

The only assumption made about the HTML is that the checkbox has an ID and that the ID starts with the string "checkbox" (thus the accompanying label for tag value starts with checkbox).

$(document).ready(function() {
    // Disable the text selection that would otherwise happen on shift+click
    $("input[type='checkbox'], label[for^='checkbox']").on("mousedown", function (e) {
        if(e.shiftKey) {
            window.getSelection().removeAllRanges();
        }
    });

    $("input[type='checkbox'], label[for^='checkbox']").on("click", function (e) {
        // We're not actually interested in the label clicks themselves
        if($(this).attr("for")){
            // Because if a label is clicked, the checkbox will be clicked too
            return;
        }

        // Get all the checkboxes
        var checkboxes = $(`input[name='${$(this).attr("name")}']`);

        // If there are no checked checkboxes, check this first one
        if(!checkboxes.filter(function() {
            return $(this).data('last-checked');
        }).length){
            $(this).data("last-checked", true);
            return;
        }

        // If the shift key is held down, select the range
        if(e.shiftKey){
            var start = checkboxes.index($(this));
            var end = checkboxes.index(checkboxes.filter(function() {
                return $(this).data('last-checked');
            }));
            checkboxes.slice(Math.min(start,end), Math.max(start,end)+ 1).prop('checked', $(this).prop("checked"));
        }

        // Deselect everything and only mark the last checked
        for (const box of checkboxes) {
            $(box).data("last-checked", false);
        }
        $(this).data("last-checked", true);
    });
});
dearsina
  • 4,774
  • 2
  • 28
  • 34
0

This is jquery solution that I wrote and use:

  • All checkboxes have same class named chksel
  • For faster individual selection a class will carry the order named chksel_index
  • Also each checkbox has an attribute named rg that contain same index

    var chksel_last=-1;
    $('.chksel').click(function(ev){
       if(ev.shiftKey){var i=0;
          if(chksel_last >=0){
            if($(this).attr('rg') >= chksel_last){
             for(i=chksel_last;i<=$(this).attr('rg');i++){$('.chksel_'+i).attr('checked','true')}}
            if($(this).attr('rg') <= chksel_last){for(i=$(this).attr('rg');i<=chksel_last;i++){$('.chksel_'+i).attr('checked','true')}}
          }  
          chksel_last=$(this).attr('rg');
       }else{chksel_last=$(this).attr('rg');}
    

    })

Zaren Wienclaw
  • 181
  • 4
  • 15
Dan
  • 1
0

Here is also another implementation similar to Outlooks multiple selection..

    <script type="text/javascript">

function inRange(x, range)
{
    return (x >= range[0] && x <= range[1]);
}

$(document).ready(function() {
    var $chkboxes = $('.chkbox');
    var firstClick = 1;
    var lastClick = null;
    var range = [];

    $chkboxes.click(function(e) {
        if(!e.shiftKey && !e.ctrlKey) {

            $('#index-' + firstClick).prop('checked', false);

            firstClick = $chkboxes.index(this) + 1;

            if (firstClick !== null && firstClick !== ($chkboxes.index(this)+1)) {
                $('#index-' + firstClick).prop('checked', true);
            }
        } else if (e.shiftKey) {
            lastClick = $chkboxes.index(this) + 1;
            if ((firstClick < lastClick) && !inRange(lastClick, range)) {
                for (i = firstClick; i < lastClick; i++) {
                    $('#index-' + i).prop('checked', true);
                }
                range = [firstClick, lastClick];
            } else if ((firstClick > lastClick) && !inRange(lastClick, range)) {
                for (i = lastClick; i < firstClick; i++) {
                    $('#index-' + i).prop('checked', true);
                }
                range = [lastClick, firstClick];
            } else if ((firstClick < lastClick) && inRange(lastClick, range)) {
                for (i = 1; i < 100; i++) {
                    $('#index-' + i).prop('checked', false);
                }

                for (i = firstClick; i < lastClick; i++) {
                    $('#index-' + i).prop('checked', true);
                }
                range = [firstClick, lastClick];
            }else if ((firstClick > lastClick) && inRange(lastClick, range)) {
                for (i = 1; i < 100; i++) {
                    $('#index-' + i).prop('checked', false);
                }

                for (i = lastClick; i < firstClick; i++) {
                    $('#index-' + i).prop('checked', true);
                }
                range = [lastClick, firstClick];
            }
        }
    });
});

vasgen
  • 81
  • 1
  • 6
0

this solution works for me, also ajax based for DataTables https://jsfiddle.net/6ouhv7bw/4/

<table id="dataTable">

<tbody>
<tr>
<td><input type="checkbox"></td>
</tr>

<tr>
<td><input type="checkbox"></td>
</tr>

<tr>
<td><input type="checkbox"></td>
</tr>

<tr>
<td><input type="checkbox"></td>
</tr>
</tbody>
</table>

<script>
$(document).ready(function() {
   var $chkboxes = $('#dataTable');
var $range = '#dataTable tbody';
var $first = false;
var $indexWrapp = 'tr';
var lastChecked = null;
var $checkboxes = 'input[type="checkbox"]';

$chkboxes.on('click',$checkboxes,function(e) {

    if ($first===false) {

        lastChecked = $(this).closest($indexWrapp).index();
        lastCheckedInput = $(this).prop('checked');
        $first=true;
        return;
    }

    if (e.shiftKey) {

        var start = lastChecked;
        var end =  $(this).closest($indexWrapp).index();

       $( $range+' '+$indexWrapp).each(function() {
          $currIndex=$(this).index();
          if( $currIndex>=start && $currIndex<=end ){
              $(this).find($checkboxes).prop('checked', lastCheckedInput);
          }

       })
    }

     lastCheckedInput = $(this).prop('checked');
     lastChecked = $(this).closest($indexWrapp).index();
});
</script>
dazzafact
  • 2,570
  • 3
  • 30
  • 49
0

Here is the Elegant implementation. The idea is to store the first selected input to the lastChecked variable and when the user selects the input field with shiftKey we will run a loop and toggle the inBetween(boolean) and mark all the checkboxes with true value. Inspired by Wesbos.

let checkboxes = document.querySelectorAll('.wrapper input[type="checkbox"]');
let lastChecked;

function logic(e) {
  let inBetween = false;
  if (e.shiftKey) {
    checkboxes.forEach(checkbox => {
      if (checkbox === this || checkbox === lastChecked) {
        inBetween = !inBetween;
      }
      if (inBetween) checkbox.checked = true;

    })
  }
  lastChecked = this;
}

checkboxes.forEach((checkbox, i) => checkbox.addEventListener('click', logic));
.wrapper {
  display: flex;
  flex-direction: column;
}
<div class="wrapper">
  <input type="checkbox" name="one">
  <input type="checkbox" name="two">
  <input type="checkbox" name="three">
  <input type="checkbox" name="four">
  <input type="checkbox" name="five">
</div>
Dejan.S
  • 18,571
  • 22
  • 69
  • 112
MVS KIRAN
  • 115
  • 1
  • 6