Drag select cells

by Alexander Tymchuk

HTML

Click and drag mouse or use shift key to select cells.
<table id="table">
  <tr>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>    
  </tr>
  <tr>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
  </tr>
  <tr>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
  </tr>
  <tr>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
  </tr>
  <tr>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
  </tr>
  <tr>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
    <td></td>
  </tr>    
</table>

CSS

table {
  border-collapse: collapse;
}
table td {
    border: 1px solid #999;
    width: 40px;
    height: 40px;
    margin: 10px;
    user-select: none;
}

td.selected {
    background-color: green;
}

JavaScript

const root = document.querySelector('#table');
let isMouseDown = false;
let rowIndex = null;
let startCellIndex = null;

function getElementIndex(element) {
  return [...element.parentNode.children].findIndex(el => element === el);
}

function removeClass(parentNode, className) {
	Array.from(parentNode.querySelectorAll('.' + className))
  	.forEach(el => el.classList.remove(className));
}

function addListeners(nodeList, eventName, listener) {
	Array.from(nodeList).forEach(node => {
  	node.addEventListener(eventName, listener, false);
  });
}

function selectTo(cell) {
    const cellIndex = getElementIndex(cell);
    let cellStart, cellEnd;
    
    if (cellIndex < startCellIndex) {
        cellStart = cellIndex;
        cellEnd = startCellIndex;
    } else {
        cellStart = startCellIndex;
        cellEnd = cellIndex;
    }
    // force to stay on the same row
    if (getElementIndex(cell.parentNode) !== rowIndex) {
 			const row = root.querySelectorAll('tr').item(rowIndex);
      [...row.children]
    		.filter((_, i) => i >= cellStart && i <= cellEnd)
    		.forEach(el => el.classList.add('selected'));
       return;
    }
  
    const { children: cells } = cell.parentNode;
    [...cell.parentNode.children]
    	.filter((_, i) => i >= cellStart && i <= cellEnd)
    	.forEach(el => el.classList.add('selected'));
}

function onMousedown(e) {
    isMouseDown = true;
    const cell = e.target;

    // deselect everything
    removeClass(root, 'selected');
    
    if (e.shiftKey) {
        selectTo(cell);                
    } else {
        cell.classList.add('selected');
        const { children: cells } = cell.parentNode;
        startCellIndex = [...cells].findIndex(td => cell === td);

        const { children: rows } = cell.parentNode.parentNode;
        rowIndex = [...rows].findIndex(row => row === cell.parentNode);
    }
    
    return false; 
    // prevent text selection
}

function onMouseover(e) {
    if (!isMouseDown) return;
  
 ...