Como selecionar várias células usando ctrl + clique

16

Eu tenho uma mesa com números. Quando clico em uma célula da tabela, alterna o estado ativo. Desejo selecionar uma célula e pressionar crtl e selecionar outra célula e, como resultado, as células entre a primeira e a segunda se tornarão ativas. Como implementá-lo?

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';
    }  
  }
}

Deve funcionar como destaque de turno e funciona nas duas direções

campo Verde
fonte
10
Essa funcionalidade normalmente não é feita com a tecla Shift?
Wimanicesir 23/03
ativo por loop, a partir lastclickde thisclicke também verificar ctrlclique
Antony Jack
Há muitas condições pouco claras - você deseja alternar em particular ou o intervalo de células, deseja destacar o deslocamento (/ alternar) do intervalo ou selecionar uma única célula, etc. Várias tescases podem esclarecer sua pergunta. ..
Tom
11
O @Wimanicesir Shift geralmente seleciona o intervalo entre o início e a corrente, enquanto o ctrl é adicionado à seleção
Emanuel Vintilă
11
@ EmanuelVintilă Mas a pergunta pede "células entre a primeira e a segunda", que é realmente o comportamento esperado da tecla Shift.
John Montgomery

Respostas:

11

Tente o seguinte:

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

awran5
fonte
6

Programei a parte Javascript c completamente diferente do que você fez. Espero que você ainda possa usá-lo. Mas faz exatamente o que você pediu.

Com Shift + Cell, você pode selecionar todas as células no meio.

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>

boa sorte ;)

SwissCodeMen
fonte
embora não seja o que OP queria, mas agradável para uma tentativa! +1
Ma'moun othman 23/03
@ Ma'mounothman ... mas ele quer que todas as células entre o primeiro e o segundo clique sejam marcadas quando você pressiona a tecla Shift ... e é assim que funciona com a minha solução. ou o que ele quer ??
SwissCodeMen 23/03
desculpe não quis dizer dessa maneira, o problema é que sua solução desmarcará células individuais selecionadas, mas não tenho certeza se está tudo bem, essa é a única coisa que precisa ser verificada, além de que sua solução funciona de qualquer maneira!
Ma'moun othman 23/03
5

Usando previousElementSiblingecompareDocumentPosition()

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;
};

Demonstração de trabalho

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

Usuário863
fonte
Não funciona ao contrário, por exemplo, selecione 6 e ctrl + selecione 2
Anurag Srivastava
@AnuragSrivastava corrigido
User863 23/03
4

Solução completa com funcionalidade para frente e para trás:

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>

MiK
fonte
4

Eu criei armazenando o índice do elemento selecionado. Funciona nos dois sentidos (2 -> 6) e (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>

CaffeinatedCod3r
fonte
4

Selecione um ou intervalo, mas se você pressionar Ctrl e clicar na terceira vez, a seleção anterior será redefinida e a nova partida será iniciada no 1º item (não é tão difícil de estender).

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>

Tom
fonte
4

Com uma pequena modificação, você pode fazer o seguinte:

<!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>

Seguindo o comentário sobre isso não funcionar para trás, re-misturei ligeiramente o original para que ele funcione nas duas direções da seleção. A versão editada utiliza datasetatributos - neste caso, atribuídos como números inteiros. É mantido um registro da célula inicial clicada e, se octrl tecla for pressionada, é feito um cálculo simples para determinar se o usuário está selecionando para frente ou para trás - o que, por sua vez, afeta o loop usado e, portanto, a atribuição da classe ativa. Um pequeno ajuste no CSS usando variáveis ​​foi apenas por conveniência ...

<!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>

RamRaider
fonte
Não funciona ao contrário, por exemplo, selecione 6 e ctrl + selecione 2
Anurag Srivastava
não estava ciente de que precisava fazer isso
RamRaider 23/03
Certo, quem precisa de uma solução completa, de qualquer maneira, amirita? :)
Anurag Srivastava 23/03
11
por que isso te preocupa? Está além do meu controle o que acontece depois que eu publico algo e abaixa para outros usuários, sobe ou desce ... a menos que você esteja sugerindo algum tipo de brincadeira em meu nome, caso em que é diferente.
RamRaider 23/03
3

Se você está aberto ao jquery, aqui está uma solução. Observe que ele não funciona na seleção reversa

$(() => {
  $(".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>

Anurag Srivastava
fonte
isso realmente não funciona no sentido inverso também
RamRaider 23/03
Minha resposta afirma isso claramente
Anurag Srivastava