Reputation:
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
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
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
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
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
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
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
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>
Upvotes: 10
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