Arrow key input to change selected row and column in a grid of HTML elements

Posted on

Problem

Below is the working code but it can surely be simplified with some sort of changeCol() and changeRow() functions, or perhaps a single function?

$(document).ready(function(e){
    let keys = {};
    let selRow = 0;
    let selCol = 0;
    let rowCount = 5;
    let colCount = 5;

    //set initial highlits

    $('[data-row="0"]').addClass('hl');
    $('[data-col="0"]').addClass('hl');

    $(document).keydown(function(event){
        keys[event.which] = true;
    }).keyup(function(event){
        delete keys[event.which];
    });
    
        for (let row = 0; row < rowCount; row++) {
        for (let col = 0; col < colCount; col++) {
            let elPiece = $("<span>", {
                "class": 'piece',
                "data-col": col,
                "data-row": row,
                text: "O"
            });
            $('#board').append(elPiece)
        }
    }

    function gameLoop() {
        // w for north
        if (keys[87]) {
            shiftCol(selCol, -1);
        }
        //press s for south
        if (keys[83]) {
            shiftCol(selCol, 1);
        }

        //up arrow
        if (keys[38]) {
            $('[data-row="' + selRow + '"]').removeClass('hl');
            $('[data-col="' + selCol + '"]').addClass('hl');
            if (selRow === 0) {
                selRow = rowCount - 1;
            } else {
                selRow--;
            }
            $('[data-row="' + selRow + '"]').addClass('hl');
        }

        //down arrow
        if (keys[40]) {
            $('[data-row="' + selRow + '"]').removeClass('hl');
            $('[data-col="' + selCol + '"]').addClass('hl');
            if (selRow === rowCount - 1) {
                selRow = 0;
            } else {
                selRow++;
            }
            $('[data-row="' + selRow + '"]').addClass('hl');
        }

        //left arrow
        if (keys[37]) {
            $('[data-col="' + selCol + '"]').removeClass('hl');
            $('[data-row="' + selRow + '"]').addClass('hl');
            if (selCol === 0) {
                selCol = colCount - 1;
            } else {
                selCol--;
            }
            $('[data-col="' + selCol + '"]').addClass('hl');
        }

        //right arrow
        if (keys[39]) {
            $('[data-col="' + selCol + '"]').removeClass('hl');
            $('[data-row="' + selRow + '"]').addClass('hl');
            if (selCol === colCount - 1) {
                selCol = 0;
            } else {
                selCol++;
            }
            $('[data-col="' + selCol + '"]').addClass('hl');
        }
        // code to move objects and repaint canvas goes here

        setTimeout(gameLoop, 100);
    }
    gameLoop();
});
#board {
  width: 150px;
  height: 150px;
}

.piece {
  width: 30px;
  height: 30px;
  display: inline-block;
  background-color: black;
  color: white;
  text-align: center;
  line-height: 30px;
}

.hl {
  background-color: gray;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="board">

</div>

Solution

Ok, after redoing it from scratch, here’s a few points to consider:

  • Instead of checking if the key “exists” check if it’s true/false
  • You can make it more flexible by making the size of the tile a variable (or even better, a variable for height and one for width)
  • It’s better to store the tiles into an array, with their respective x and y variable. This way, you don’t have to parse the DOM with jQuery each iteration of the loop, you only loop through the array and select the tile element.

Here’s the new code:

HTML – Really simple, just the container for the tiles

<div class="board"></div>

CSS – We don’t set the size of anything, we’ll calculate it with JS

*{
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

body{
  background: #f1f1f1;
}

html,
body{
  width: 100%;
  height: 100%;
  display: flex;
  align-items: center;
  justify-content: center;
}

.board{
  display: flex;
  flex-wrap: wrap;
}

.tile{
  background-color: #000;
  color: #fff;
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: .75rem;
}

.active{
  background: #aaa;
}

JS

//amount of tiles horinzontally and vertically
var tileAmountX = 15;
var tileAmountY = 15;


//actual column/row
var actualX = 0;
var actualY = 0;

//size in pixels of the tiles
var tileWidth = 40;
var tileHeight = 40;

//size in pixels of the board, calculated with tile size and tile amount
var boardWidth = tileWidth * tileAmountX;
var boardHeight = tileHeight * tileAmountY;


//array for storing the tiles
var tiles = [];

//keys object, default state are all off (false)
var keys = {left: false, top: false, right: false, down: false};

//set board size style
$('.board').css({
  width: boardWidth + 'px',
  height: boardHeight + 'px'
});

//generate tiles
for(var i = 0; i < tileAmountX; i++){
  for(var j = 0; j < tileAmountY; j++){    

    //create tile and store it in temporary variable
    var el = $('<div class="tile" style="width: '+tileWidth+'px; height: '+tileHeight+'px;">O</div>').appendTo('.board');

    //store that variable in the `tiles` array, with the respective `x` and `y` coordinate
    tiles.push({
      el: el,
      x: i,
      y: j
    });    
  }
}

//move to right function, just changes the actualY value and check for overflow
function moveRight(){ 
  actualY = actualY < tileAmountY - 1 ? actualY + 1 : 0;
}

//move to left function, just changes the actualY value and check for overflow
function moveLeft(){
  actualY = actualY > 0 ? actualY - 1 : tileAmountY - 1;
}

//move to top function, just changes the actualX value and check for overflow
function moveTop(){
  actualX = actualX > 0 ? actualX - 1 : tileAmountX - 1;
}

//move to down function, just changes the actualX value and check for overflow
function moveDown(){
  actualX = actualX < tileAmountX - 1 ? actualX + 1 : 0;
}

//generic move function
function move(){
  //get the tiles that have the X or Y coordinate from actualX or actualY
  var starting = tiles.filter(function(val){
    return (val.x == actualX || val.y == actualY);
  });

  //remove the active class for all tiles
  for(var i = 0; i < tiles.length; i++){
    var tile = tiles[i];
    tile.el.removeClass('active');
  }

  //add the active class for the tiles that have the X or Y coordinate from actualX and actualY
  for(var i = 0; i < starting.length; i++){
     var tile = starting[i];
     tile.el.addClass('active');
  }
}

function loop(){

  //this is pretty self explanatory
  if(keys.right){
    moveRight();
  }else if(keys.left){
    moveLeft();
  }

  if(keys.top){
    moveTop();
  }else if(keys.down){
    moveDown();
  }

  //this function runs regardless of the user input
  move();

  //loop with 30fps
  setTimeout(loop, 1000/30);
}

//initiate loop
loop();

//event listener for keydown and keyup
$(window).on('keydown', function(e){
  if(e.keyCode==37){keys.left = true}else if(e.keyCode==39){keys.right = true};
  if(e.keyCode==38){keys.top = true}else if(e.keyCode==40){keys.down = true};
});

$(window).on('keyup', function(e){
  if(e.keyCode==37){keys.left = false}else if(e.keyCode==39){keys.right = false};
  if(e.keyCode==38){keys.top = false}else if(e.keyCode==40){keys.down = false};
});

If you want to see a live example, check this link:

https://codepen.io/tyrellrummage/full/bobxPp/

As you can see, even with a big board (15×15) the motion is very fluid and responsive.

There may be a better way to achieve this, but this is the best I can come up with!

Leave a Reply

Your email address will not be published. Required fields are marked *