16

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

6
  • 10
    Isn't this functionality normally done with the shift key? Commented Mar 23, 2020 at 8:32
  • active by loop, from lastclick to thisclick and also check for ctrl click Commented Mar 23, 2020 at 8:34
  • There is a lot of unclear conditions - do you want to toggle particular or range of cells still, do you want to make a shift-like highlighting (/toggle) of range or select single cell, etc. Several tescases may clear your question... Commented Mar 23, 2020 at 10:40
  • 1
    @Wimanicesir Shift usually selects the range between the start and current, while ctrl adds to the selection Commented Mar 23, 2020 at 21:00
  • 1
    @EmanuelVintilă But the question asks for "cells between first one and second," which is indeed the expected behavior of the Shift key. Commented Mar 23, 2020 at 22:01

8 Answers 8

10

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

Sign up to request clarification or add additional context in comments.

Comments

5

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>

3 Comments

although its not what OP wanted but nice for a try! +1
@Ma'mounothman ...but he wants all cells between the first and the second click to be marked when you press the shift key... and this is how it works with my solution. or what does he want??
sorry didn't mean it that way, the thing is your solution will de-selected individual cells selected but not sure if its Ok, this is the only thing that need to be check, other than that your solution works anyway!
4

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

1 Comment

Doesn't work in reverse, eg select 6 and ctrl + select 2
4

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>

4 Comments

Doesn't work in reverse, eg select 6 and ctrl + select 2
wasn't aware that it needed to do that
Right, who needs a complete solution anyway amirite? :)
why does it concern you? It's beyond my control what happens after I post something and entirely down to other users whether it goes up or down... unless you are suggesting some jiggery pokery on my behalf in which case that is different.
3

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>

Comments

3

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>

Comments

3

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>

Comments

2

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>

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.