How to select multiple cells using ctrl + click

Viewed 2033

I have a table with numbers. When I click on a cell in the table, it toggles active state. I want to select one cell and press crtl and select another cell, and as result cells between first one and second will become active. How to implement it?

codepen https://codepen.io/geeny273/pen/GRJXBQP

<div id="grid">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
</div>
const grid = document.getElementById("grid")

grid.onclick = (event) => {
  event.stopPropagation();
  const { className } = event.target;

  if (className.includes('cell')) {
    if (className.includes('active')) {
      event.target.className = 'cell';
    } else {
      event.target.className = 'cell active';
    }  
  }
}

It should work like shift highlighting and works in both directions

8 Answers

Try this:

const cells = document.querySelectorAll(".cell");
let lastClicked;

function handleClick(e) {
  // Toggle class active
  if (e.target.classList.contains("active")) {
    e.target.classList.remove("active");
  } else {
    e.target.classList.add("active");
  }

  // Check if CTRL key is down and if the clicked cell has aready class active
  let inRange = false;
  if (e.ctrlKey && this.classList.contains("active")) {
    // loop over cells
    cells.forEach(cell => {
      // check for the first and last cell clicked
      if (cell === this || cell === lastClicked) {
        // reverse inRange
        inRange = !inRange;
      }
      // If we are in range, add active class
      if (inRange) {
        cell.classList.add("active");
      }
    });
  }
  // Mark last clicked
  lastClicked = this;
}

cells.forEach(cell => cell.addEventListener("click", handleClick));
#grid {
  display: grid;
  grid-template-columns: repeat(3, 50px);
  grid-template-rows: repeat(2, 50px);
}

.cell {
  display: flex;
  justify-content: center;
  align-items: center;
  border: solid 1px #ccc;
}

.active {
  background-color: #80aaff;
}
<div id="grid">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
</div>

codepen

I programmed the Javascript part completely different than you did. I hope that you can still use it. But it does exactly what you asked for.

With Shift + Cell you can select all cells in between.

var $lastSelected = [],
    container     = $('#grid'),
    collection    = $('.cell');

container.on('click', '.cell', function(e) {
    var that = $(this),
        $selected,
        direction;

    if (e.shiftKey){

        if ($lastSelected.length > 0) {
             
            if(that[0] == $lastSelected[0]) {
                return false;
            }
      
            direction = that.nextAll('.lastSelected').length > 0 ? 'forward' : 'back';
 
            if ('forward' == direction) {
                // Last selected is after the current selection
                $selected = that.nextUntil($lastSelected, '.cell');
 
            } else {
                // Last selected is before the current selection
                $selected = $lastSelected.nextUntil(that, '.cell');
            }
             
            collection.removeClass('selected');
            $selected.addClass('selected');
            $lastSelected.addClass('selected');
            that.addClass('selected');
 
        } else {
            $lastSelected = that;
            that.addClass('lastSelected');
            collection.removeClass('selected');
            that.addClass('selected');
        }

    } else {
        $lastSelected = that;
        collection.removeClass('lastSelected selected');
        that.addClass('lastSelected selected');
   }
});
.selected {background-color: #80aaff;}
#grid{
  display: grid;
  grid-template-columns: repeat(3, 50px);
  grid-template-rows: repeat(2, 50px);
}

.cell {
  display: flex;
  justify-content: center;
  align-items: center;
  border: solid 1px #ccc;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div id="grid">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
</div>

With a slight modification you can do it like this:

<!DOCTYPE html>
<html lang='en'>
    <head>
        <meta charset='utf-8' />
        <title></title>
        <style>
            #grid {
              display: grid;
              grid-template-columns: repeat(3, 50px);
              grid-template-rows: repeat(2, 50px);
            }
            
            .cell {
              display: flex;
              justify-content: center;
              align-items: center;
              border: solid 1px #ccc;
            }
            
            .active {
              background-color: #80aaff;
            }
        </style>
        <script>
            document.addEventListener('DOMContentLoaded',e=>{
                const grid = document.getElementById('grid')
                const cells= grid.querySelectorAll('div');
                
                grid.addEventListener('click',function(e){
                    e.stopPropagation();
                    
                    cells.forEach( cell=>{
                        cell.classList.remove('active')
                    });
                    event.target.classList.add('active');
                    
                    if( event.ctrlKey ) {
                        Array.from(cells).some( cell=>{
                            cell.classList.add('active')
                            if( cell==event.target )return true;
                        })
                    }
                });
            });
        </script>
    </head>
    <body>
        <div id="grid">
          <div class="cell">1</div>
          <div class="cell">2</div>
          <div class="cell">3</div>
          <div class="cell">4</div>
          <div class="cell">5</div>
          <div class="cell">6</div>
        </div>
    </body>
</html>

document.addEventListener('DOMContentLoaded',e=>{
const grid = document.getElementById('grid')
const cells= grid.querySelectorAll('div');

grid.addEventListener('click',function(e){
  e.stopPropagation();

  cells.forEach( cell=>{
    cell.classList.remove('active')
  });
  e.target.classList.add('active');

  if( e.ctrlKey ) {
    Array.from(cells).some( cell=>{
      cell.classList.add('active')
      if( cell==e.target )return true;
    })
  }
});
});
#grid {
  display: grid;
  grid-template-columns: repeat(3, 50px);
  grid-template-rows: repeat(2, 50px);
}

.cell {
  display: flex;
  justify-content: center;
  align-items: center;
  border: solid 1px #ccc;
}

.active {
  background-color: #80aaff;
}
<div id="grid">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
</div>

Following on from the comment regarding this not working backwards I re-hashed the original slightly so that it does work in both directions of selection. The edited version makes use of dataset attributes - in this case assigned as integers. A record is kept of initial cell clicked and, if the ctrl key is pressed a simple calculation is done to determine if the user is selecting forwards or backwards - which in turn affects the loop used and thus the assignment of the active class. A minor tweak to the CSS using variables was just for convenience...

<!DOCTYPE html>
<html lang='en'>
    <head>
        <meta charset='utf-8' />
        <title></title>
        <style>
            :root{
                --rows:2;
                --cols:3;
                --size:50px;
            }
            #grid {
              display:grid;
              grid-template-columns:repeat(var(--cols),var(--size));
              grid-template-rows:repeat(var(--rows),var(--size));
              width:calc(var(--size) * var(--cols));
            }
            
            .cell {
              display: flex;
              flex:1;
              justify-content: center;
              align-items: center;
              border: solid 1px #ccc;
              margin:1px;
              cursor:pointer;
            }
            
            .active {
              background-color: #80aaff;
            }
        </style>
        <script>
            document.addEventListener('DOMContentLoaded',e=>{

                let range=[];
                
                const grid  = document.getElementById('grid')
                const cells = grid.querySelectorAll('div');
                
                const getcell=function(i){
                    return grid.querySelector('[data-index="'+i+'"]');
                }
                const clickhandler=function(e){
                    e.stopPropagation();
                    range.push( e.target );
                    
                    /* clear cells of the "active" class */
                    cells.forEach( cell=>{
                        cell.classList.remove('active')
                    });
                    /* Assign the initially selected cell as "active" */
                    e.target.classList.add('active');
                    
                    
                    if( e.ctrlKey ) {
                        /* Is the user selecting forwards or backwards? */
                        if( range[0].dataset.index < e.target.dataset.index ){
                            for( let i=range[0].dataset.index; i < e.target.dataset.index; i++ )getcell(i).classList.add('active')
                        } else if( range[0].dataset.index == e.target.dataset.index ){
                            e.target.classList.add('active')
                        } else {
                            for( let i=range[0].dataset.index; i > e.target.dataset.index; i-- )getcell(i).classList.add('active')
                        }
                        
                        range=[];
                    }
                };
                
                /* assign an integer index to each cell within parent */
                cells.forEach( ( cell, index )=>{
                    cell.dataset.index = index + 1;
                });
                
                grid.addEventListener( 'click', clickhandler );
            });
        </script>
    </head>
    <body>
        <div id="grid">
          <div class="cell">1</div>
          <div class="cell">2</div>
          <div class="cell">3</div>
          <div class="cell">4</div>
          <div class="cell">5</div>
          <div class="cell">6</div>
        </div>
    </body>
</html>

document.addEventListener('DOMContentLoaded',e=>{

  let range=[];

  const grid  = document.getElementById('grid')
  const cells = grid.querySelectorAll('div');

  const getcell=function(i){
    return grid.querySelector('[data-index="'+i+'"]');
  }
  const clickhandler=function(e){
    e.stopPropagation();
    range.push( e.target );

    /* clear cells of the "active" class */
    cells.forEach( cell=>{
      cell.classList.remove('active')
    });
    /* Assign the initially selected cell as "active" */
    e.target.classList.add('active');


    if( e.ctrlKey ) {
      /* Is the user selecting forwards or backwards? */
      if( range[0].dataset.index < e.target.dataset.index ){
        for( let i=range[0].dataset.index; i < e.target.dataset.index; i++ )getcell(i).classList.add('active')
      } else if( range[0].dataset.index == e.target.dataset.index ){
        e.target.classList.add('active')
      } else {
        for( let i=range[0].dataset.index; i > e.target.dataset.index; i-- )getcell(i).classList.add('active')
      }

      range=[];
    }
  };

  /* assign an integer index to each cell within parent */
  cells.forEach( ( cell, index )=>{
    cell.dataset.index = index + 1;
  });

  grid.addEventListener( 'click', clickhandler );
});
:root{
  --rows:2;
  --cols:3;
  --size:50px;
}
#grid {
  display:grid;
  grid-template-columns:repeat(var(--cols),var(--size));
  grid-template-rows:repeat(var(--rows),var(--size));
  width:calc(var(--size) * var(--cols));
}

.cell {
  display: flex;
  flex:1;
  justify-content: center;
  align-items: center;
  border: solid 1px #ccc;
  margin:1px;
  cursor:pointer;
}

.active {
  background-color: #80aaff;
}
<div id="grid">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
</div>

Using previousElementSibling and compareDocumentPosition()

const grid = document.getElementById("grid");
const cells = [...grid.querySelectorAll(".cell")];
let recentActive;

grid.onclick = event => {
  event.stopPropagation();
  const { className } = event.target;

  if (!className.includes("cell")) {
    return;
  }

  let compareMask = recentActive && recentActive.compareDocumentPosition(event.target);
  let property = compareMask == 2 ? "nextElementSibling" : "previousElementSibling";

  let state = event.target.classList.toggle("active");
  let sibiling = event.target[property];

  while (event.ctrlKey && state && !sibiling.classList.contains("active")) {
    sibiling.classList.add("active");
    sibiling = sibiling[property];
  }
  recentActive = event.target;
};

Working Demo

https://codepen.io/aswinkumar863/pen/QWbVVNG

I have created by storing index of selected element. It work in both ways (2 -> 6) and (6 -> 2)

const grid = document.getElementById("grid")

var cells = []

function activate_cell(min, max) {

    for (var i = 0; i < grid.children.length; i++) {
        // Clear all selection
        var el = Array.from(grid.children)[i]
        el.classList.remove("active");
    }
    for (var i = min; i <= max; i++) {
        var el = Array.from(grid.children)[i]
        el.classList.toggle("active");
    }
}
grid.onclick = (event) => {
    event.stopPropagation();
    const { className } = event.target;

    const index = Array.from(grid.children).indexOf(event.target)
    cells.push(index)
    if (event.ctrlKey) {
        activate_cell(Math.min(...cells), Math.max(...cells))
    } else {
        cells.length = 0  // Empty selection if ctrl is not pressed
        cells.push(index)
        activate_cell(Math.min(...cells), Math.max(...cells))
    }
}
#grid {
    display: grid;
    grid-template-columns: repeat(3, 50px);
    grid-template-rows: repeat(2, 50px);
}

.cell {
    display: flex;
    justify-content: center;
    align-items: center;
    border: solid 1px #ccc;
}

.active {
    background-color: #80aaff;
}
<div id="grid">
    <div class="cell">1</div>
    <div class="cell">2</div>
    <div class="cell">3</div>
    <div class="cell">4</div>
    <div class="cell">5</div>
    <div class="cell">6</div>
</div>

Complete solution with forwards and backwards functionality:

const grid = document.getElementById("grid");
var lastactive = "";

grid.onclick = (event) => {
  event.stopPropagation();
  const { className } = event.target;
  
  if (className.includes('cell')) {
    if (className.includes('active')) {
      event.target.className = 'cell';
      if(lastactive != "" && event.target === lastactive) {
        lastactive = "";
        let cells = document.querySelectorAll('.cell');
        for(let i = 0; i < cells.length; i++) {
          if(cells[i].className.includes('active')) {
            lastactive = cells[i];
            break;
          }
        }
      }
    } 
    else {
      event.target.className = 'cell active';
      if(event.ctrlKey && lastactive != "") {
        let current = event.target;
        if(event.target.compareDocumentPosition(lastactive) == 4 /*event target is before or after last active?*/) {
          while(current != lastactive) {
             current.className = 'cell active';
             current = current.nextElementSibling;
          }
        }
        else {
          while(current != lastactive) {
             current.className = 'cell active';
             current = current.previousElementSibling;
          }
        }
      }
      lastactive = event.target;
    }  
  }
  console.log(lastactive);
}
#grid {
  display: grid;
  grid-template-columns: repeat(3, 50px);
  grid-template-rows: repeat(3, 50px);
}

.cell {
  display: flex;
  justify-content: center;
  align-items: center;
  border: solid 1px #ccc;
  cursor: pointer;
  user-select: none;
}

.active {
  background-color: #80aaff;
}
<div id="grid">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
  <div class="cell">7</div>
  <div class="cell">8</div>
  <div class="cell">9</div>
</div>

Select one or interval, but if you press Ctrl and click 3rd time previous selection is reset and new starts from 1st item (not so hard to extend).

const grid = document.getElementById("grid")
var previousCell = [];

function toggle(event) {
  event.stopPropagation();
  var target = event.target;

  if (target.className.indexOf('cell') > -1) {
    var cells = target.parentElement.getElementsByClassName("cell");
    if (event.ctrlKey || previousCell[0] == previousCell[1]) {
      if (!event.ctrlKey) previousCell = [];
      previousCell.push(target);
      prepareRange(cells, previousCell);
      switchRange(cells, previousCell);
      previousCell = [target];
      prepareRange(cells, previousCell);
    }
    document.getElementById("range").innerText = previousCell[0]+1;
  }
}
function prepareRange(cells, previousCells) {
  for(var i=0;i<cells.length;i++) {
    var pos = previousCell.indexOf(cells[i]);
    if (pos > -1 && previousCell.length < 4) {
      previousCell.push(i);
    }
  }
  if (previousCell.length == 2) {
    previousCell[0] = previousCell[1];
  } else {
    previousCell[1] = previousCell.pop();
    previousCell.pop();
    previousCell.sort();
  }
}
function switchRange(cells, previousCells) {
  for(var i = previousCells[0];i <= previousCells[1]; i++) {
    target = cells[i];
    if (target.className.indexOf('active') > -1) {
      target.className = 'cell';
    } else {
      target.className = 'cell active';
    }
    if (previousCell.length == 1) break;
  }
}
#grid {
  display: grid;
  grid-template-columns: repeat(3, 50px);
  grid-template-rows: repeat(2, 50px);
}

.cell {
  display: flex;
  justify-content: center;
  align-items: center;
  border: solid 1px #ccc;
}

.active {
  background-color: #80aaff;
}
<div id="grid" onclick="toggle(event)">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
</div>
Last cell:<div id="range"></div>

If you are open to jquery, here's a solution. Note that it doesn't work in reverse selection

$(() => {
  $(".cell").on("click", function(e) {
    $(this).toggleClass("active")
    if (e.ctrlKey) {
      $(this).prevUntil(".active").addClass("active")
    }
  })
})
#grid {
  display: grid;
  grid-template-columns: repeat(3, 50px);
  grid-template-rows: repeat(2, 50px);
}

.cell {
  display: flex;
  justify-content: center;
  align-items: center;
  border: solid 1px #ccc;
}

.active {
  background-color: #80aaff;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="grid">
  <div class="cell">1</div>
  <div class="cell">2</div>
  <div class="cell">3</div>
  <div class="cell">4</div>
  <div class="cell">5</div>
  <div class="cell">6</div>
</div>

Related