'How can I select a square of cells (any size) and snap to grid?

How can I add a mouse event to select a range (up and down/left and right) of cells in my grid?

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Billion Satoshis</title>
    <script type="module">
      let cells = Math.pow(256, 2);
      document.addEventListener("DOMContentLoaded", main);

      function main() {
        const view = document.getElementById("scroller");
        const grid = [];

        for (let i = 0; i < cells; i++) {
          const el = document.createElement("div");

          el.classList.add("cell");
          grid.push(el);
        }

        view.append(...grid);

        view.style.gridTemplateColumns = `repeat(${Math.sqrt(cells)}, 10px)`;

        /* added */
        const selectionLayer = document.getElementById("selectionLayer");
        let isMouseDown = false;
        let isMultiSelectionOn = false;
        let selectionStart = {},
          selectionEnd = {};
        let selectionRect = {};
        let scrollBarCorrection_x = view.offsetWidth - view.clientWidth;
        let scrollBarCorrection_y = view.offsetHeight - view.clientHeight;
        let selectionMargin = {
          width: 10,
          height: 5,
        };

        view.addEventListener("mousedown", (e) => {
          isMouseDown = true;

          if (!isMultiSelectionOn) {
            selectionEnd = {
              x: null,
              y: null,
            };
            clearSecelction();
          }

          selectionStart = {
            x: e.x - view.offsetLeft,
            y: e.y - view.offsetTop,
          };
        });

        view.addEventListener("mouseup", (e) => {
          isMouseDown = false;

          selectionEnd = {
            x: e.x - view.offsetLeft,
            y: e.y - view.offsetTop,
          };

          if (
            Math.abs(selectionEnd.x - selectionStart.x) <
              selectionMargin.width ||
            Math.abs(selectionEnd.y - selectionStart.y) < selectionMargin.height
          )
            return;

          selectionLayer.style.top = `0px`;
          selectionLayer.style.left = `0px`;
          selectionLayer.style.bottom = "unset";
          selectionLayer.style.right = "unset";
          selectionLayer.style.visibility = "hidden";

          getSelectedCells().forEach((c) => c.classList.add("selected"));
        });

        view.addEventListener("mousemove", (e) => {
          if (!isMouseDown) return;

          selectionEnd = {
            x: e.x,
            y: e.y,
          };

          selectionRect = {
            x1: Math.min(selectionStart.x, selectionEnd.x),
            y1: Math.min(selectionStart.y, selectionEnd.y),
            x2:
              Math.max(selectionStart.x, selectionEnd.x) +
              scrollBarCorrection_x,
            y2:
              Math.max(selectionStart.y, selectionEnd.y) +
              scrollBarCorrection_y,
          };

          selectionLayer.style.top = `${Math.min(
            selectionRect.y1,
            selectionRect.y2
          )}px`;
          selectionLayer.style.left = `${Math.min(
            selectionRect.x1,
            selectionRect.x2
          )}px`;
          selectionLayer.style.bottom = `${
            view.offsetHeight +
            view.offsetTop -
            Math.max(selectionRect.y1, selectionRect.y2)
          }px`;
          selectionLayer.style.right = `${
            view.offsetWidth +
            view.offsetLeft -
            Math.max(selectionRect.x1, selectionRect.x2)
          }px`;

          selectionLayer.style.visibility = "visible";

          if (!isMultiSelectionOn) clearSecelction();
        });

        function clearSecelction() {
          view
            .querySelectorAll(".cell")
            .forEach((c) => c.classList.remove("selected"));
        }

        function getSelectedCells() {
          let selectedCells = [];
          const cells = view.querySelectorAll(".cell");
          cells.forEach((c) => {
            const bounds = c.getBoundingClientRect();

            selectionLayer.style.visibility = "visible";

            if (
              selectionRect.x1 <= bounds.left - view.offsetLeft &&
              selectionRect.y1 <= bounds.top - view.offsetTop &&
              selectionRect.x2 - scrollBarCorrection_x >=
                bounds.right - view.offsetLeft &&
              selectionRect.y2 - scrollBarCorrection_y >= bounds.bottom
            )
              selectedCells.push(c);
          });
          return selectedCells;
        }

        document.addEventListener("keydown", (e) => {
          if (e.ctrlKey) isMultiSelectionOn = true;
        });
        document.addEventListener("keyup", (e) => {
          if (!e.ctrlKey) isMultiSelectionOn = false;
        });
        document.addEventListener("resize", () => {
          scrollBarCorrection_x = view.offsetWidth - view.clientWidth;
          scrollBarCorrection_y = view.offsetHeight - view.clientHeight;
        });
      }
    </script>
    <style>
      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }

      html {
        font-size: 62.5%;
      }

      body {
        font-size: 1.6rem;
        font-family: sans-serif;
      }

      h1 {
        font-size: 2rem;
      }

      #view {
        width: calc(100vw - 2rem);
        height: calc(100vh - 10rem);
        margin: auto;
        background-color: #eee;
        border: 1px solid black;
        overflow: auto;
        display: grid;
        gap: 0;
        border-top: 1px solid black;
        border-left: 1px solid black;
        position: relative;
        user-select: none;
      }

      header {
        text-align: center;
      }

      .cell {
        width: 1rem;
        height: 1rem;
        border-right: 1px solid black;
        border-bottom: 1px solid black;
      }

      .cell.selected {
        background-color: #5eba7d;
      }

      #scroller {
        position: absolute;
        height: 100%;
        width: 100%;
        display: grid;
        grid-template-columns: repeat(640, 10px);
        gap: 0;
      }

      #selectionLayer {
        position: absolute;
        top: 0px;
        left: 0px;
        border: 1px solid #5eba7d;
        background-color: #5eba7d45;
        z-index: 99999;
      }
    </style>
  </head>

  <body>
    <header>
      <h1>Billion Satoshis</h1>
      <p>Pixel based advertising for crypto</p>
      <p><small>$1/square (svg only)</small></p>
    </header>
    <section id="view">
      <div id="scroller">
        <div id="selectionLayer"></div>
      </div>
    </section>
  </body>
</html>

Coloring the selected cells red is fine for example.

https://jsfiddle.net/chovy/cs36ykrm/



Solution 1:[1]

Try bellow code. [UPDATED]

  1. Modified your existing code. Added .cell class user-select: none; so cell wont get dragged on mouse move.

  2. With mousedown, mousedown and mousemove you can track selection range and add whatever attributes you want.

let cells = 6400;
document.addEventListener("DOMContentLoaded", main);

function main() {
    const view = document.getElementById("view");
    const grid = [];

    for (let i = 0; i < cells; i++) {
        const el = document.createElement("div");

        el.classList.add("cell");
        grid.push(el);
    }

    view.append(...grid);

    /* added */
    const selectionLayer = document.getElementById("selectionLayer");
    let isMouseDown = false;
    let isMultiSelectionOn = false;
    let selectionStart = {},
        selectionEnd = {};
    let selectionRect = {};
    let scrollBarCorrection_x = view.offsetWidth - view.clientWidth;
    let scrollBarCorrection_y = view.offsetHeight - view.clientHeight;
    let selectionMargin = {
        width: 10,
        height: 5,
    }


    view.addEventListener("mousedown", (e) => {
        isMouseDown = true;

        if (!isMultiSelectionOn) {
            selectionEnd = {
                x: null,
                y: null
            };
            clearSecelction();
        }

        selectionStart = {
            x: e.x - view.offsetLeft,
            y: e.y - view.offsetTop
        };
    });

    view.addEventListener("mouseup", (e) => {
        isMouseDown = false;

        selectionEnd = {
            x: e.x - view.offsetLeft,
            y: e.y - view.offsetTop
        };

        if (
            Math.abs(selectionEnd.x - selectionStart.x) < selectionMargin.width ||
            Math.abs(selectionEnd.y - selectionStart.y) < selectionMargin.height
        ) return;

        selectionLayer.style.top = `0px`;
        selectionLayer.style.left = `0px`;
        selectionLayer.style.bottom = "unset";
        selectionLayer.style.right = "unset";
        selectionLayer.style.visibility = "hidden";

        getSelectedCells().forEach((c) => c.classList.add("selected"));
    });

    view.addEventListener("mousemove", (e) => {
        if (!isMouseDown) return;

        selectionEnd = {
            x: e.x,
            y: e.y
        };

        selectionRect = {
            x1: Math.min(selectionStart.x, selectionEnd.x),
            y1: Math.min(selectionStart.y, selectionEnd.y),
            x2: Math.max(selectionStart.x, selectionEnd.x) + scrollBarCorrection_x,
            y2: Math.max(selectionStart.y, selectionEnd.y) + scrollBarCorrection_y
        };

        selectionLayer.style.top = `${Math.min(
            selectionRect.y1,
            selectionRect.y2
        )}px`;
        selectionLayer.style.left = `${Math.min(
            selectionRect.x1,
            selectionRect.x2
        )}px`;
        selectionLayer.style.bottom = `${view.offsetHeight +
            view.offsetTop -
            Math.max(selectionRect.y1, selectionRect.y2)
            }px`;
        selectionLayer.style.right = `${view.offsetWidth +
            view.offsetLeft -
            Math.max(selectionRect.x1, selectionRect.x2)
            }px`;

        selectionLayer.style.visibility = "visible";

        if (!isMultiSelectionOn)
            clearSecelction();
    });

    function clearSecelction() {
        view.querySelectorAll(".cell")
            .forEach((c) => c.classList.remove("selected"));
    }

    function getSelectedCells() {
        let selectedCells = [];
        const cells = view.querySelectorAll(".cell");
        cells.forEach((c) => {
            const bounds = c.getBoundingClientRect();

            selectionLayer.style.visibility = "visible";

            if (
                selectionRect.x1 <= (bounds.left - view.offsetLeft) &&
                selectionRect.y1 <= (bounds.top - view.offsetTop) &&
                (selectionRect.x2 - scrollBarCorrection_x) >= (bounds.right - view.offsetLeft) &&
                (selectionRect.y2 - scrollBarCorrection_y) >= bounds.bottom
            )
                selectedCells.push(c);
        });
        return selectedCells;
    }

    document.addEventListener("keydown", (e) => {
        if (e.ctrlKey) isMultiSelectionOn = true;
    });
    document.addEventListener("keyup", (e) => {
        if (!e.ctrlKey) isMultiSelectionOn = false;
    });
    document.addEventListener('resize', () => {
        scrollBarCorrection_x = view.offsetWidth - view.clientWidth;
        scrollBarCorrection_y = view.offsetHeight - view.clientHeight;
    })
}
* {
    box-sizing: border-box;
    margin: 0;
    padding: 0;
}

html {
    font-size: 62.5%;
}

body {
    font-size: 1.6rem;
    font-family: sans-serif;
}

#view {
    width: calc(100vw - 2rem);
    height: calc(100vh - 2rem);
    /*margin: auto;*/
    background-color: #eee;
    border: 1px solid black;
    overflow: auto;
    display: grid;
    grid-template-columns: repeat(80, 1rem);
    gap: 0;
    border-top: 1px solid black;
    border-left: 1px solid black;

    /*added*/
    position: relative;
    -moz-user-select: -moz-none;
    -khtml-user-select: none;
    -webkit-user-select: none;
    -ms-user-select: none;
    user-select: none;
}


header {
    text-align: center;
}

.cell {
    width: 1rem;
    height: 1rem;
    border-right: 1px solid black;
    border-bottom: 1px solid black;
}

.cell.selected {
    background-color: #5eba7d;
}

#selectionLayer {
    position: absolute;
    top: 0px;
    left: 0px;
    border: 1px solid #5eba7d;
    background-color: #5eba7d45;
    z-index: 99999;
}
<header>
   <h1>Billion Satoshis</h1>
   <p>Pixel based advertising for Bitcoin.</p>
</header>
<section id="view">
    <div id='selectionLayer'></div>
</section>

This is working demo. You can improve and optimise.

You can add div to show selection layer on mousemove. You can correct selectionLayer position setting relative to view container.

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1