user11284257
user11284257

Reputation:

How to select multiple cells using ctrl + click

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

Upvotes: 16

Views: 3343

Answers (8)

Professor Abronsius
Professor Abronsius

Reputation: 33813

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>

Upvotes: 4

Jan
Jan

Reputation: 2249

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>

Upvotes: 3

CaffeinatedCod3r
CaffeinatedCod3r

Reputation: 882

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>

Upvotes: 3

User863
User863

Reputation: 20039

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

Upvotes: 4

SwissCodeMen
SwissCodeMen

Reputation: 4885

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>

Upvotes: 5

MiK
MiK

Reputation: 913

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>

Upvotes: 3

awran5
awran5

Reputation: 4536

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

Upvotes: 10

Anurag Srivastava
Anurag Srivastava

Reputation: 14413

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>

Upvotes: 2

Related Questions