Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to calculate the amount of flexbox items in a row?

A grid is implemented using the CSS flexbox. Example:

enter image description here

The number of rows in this example is 4 because I fixed the container width for demo purposes. But, in reality, it can change based on container's width (e.g. if the user resizes the window). Try to resize the Output window in this example to get a feeling.

There is always one active item, marked with the black border.

Using JavaScript, I allow users to navigate to the previous/next item using the left/right arrow. In my implementation, I just decrease/increase the index of the active item by 1.

Now, I'd like to allow users to navigate up/down as well. For that, I just need to decrease/increase the index of the active item by <amount of items in a row>. But, how do I calculate this number given that it is dependent on container's width? Is there a better way to implement the up/down functionality?

.grid {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  width: 250px;
  height: 200px;
  background-color: #ddd;
  padding: 10px 0 0 10px;
}

.item {
  width: 50px;
  height: 50px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item active"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>
like image 649
Misha Moroshko Avatar asked Mar 01 '18 06:03

Misha Moroshko


People also ask

How do you get 3 items per row on flexbox?

For 3 items per row, add on the flex items: flex-basis: 33.333333% You can also use the flex 's shorthand like the following: flex: 0 0 33.333333% => which also means flex-basis: 33.333333% .

How do you flex items in one row?

There is no method in flexbox to tell items in one row to line up with items in the row above — each flex line acts like a new flex container. It deals with space distribution across the main axis.

How many possible values can flex-direction have?

The main axis is defined by flex-direction , which has four possible values: row. row-reverse. column.


4 Answers

The question is slightly more complex than finding how many items are in a row.

Ultimately, we want to know if there's an element above, below, left, and right of the active element. And this needs to account for cases where the bottom row is incomplete. For example, in the case below, the active element has no item above, below, or right:

enter image description here

But, in order to determine if there's an item above/below/left/right of the active item, we need to know how many items are in a row.

Find the number of items per row

To get the number of items per row we need:

  • itemWidth - the outerWidth of a single element including border, padding and margin
  • gridWidth - the innerWidth of the grid, excluding border, padding and margin

To calculate these two values with plain JavaScript we can use:

const itemStyle = singleItem.currentStyle || window.getComputedStyle(active);
const itemWidth = singleItem.offsetWidth + parseFloat(itemStyle.marginLeft) + parseFloat(itemStyle.marginRight);

const gridStyle = grid.currentStyle || window.getComputedStyle(grid);
const gridWidth = grid.clientWidth - (parseFloat(gridStyle.paddingLeft) + parseFloat(gridStyle.paddingRight));

Then we can calculate the number of elements per row using:

const numPerRow = Math.floor(gridWidth / itemWidth)

Note: this will only work for uniform-sized items, and only if the margin is defined in px units.

A Much, Much, Much Simpler Approach

Dealing with all these widths, and paddings, margins, and borders is really confusing. There's a much, much, much simpler solution.

We only need to find the index of the grid element who's offsetTop property is greater than the first grid element's offsetTop.

const grid = Array.from(document.querySelector("#grid").children);
const baseOffset = grid[0].offsetTop;
const breakIndex = grid.findIndex(item => item.offsetTop > baseOffset);
const numPerRow = (breakIndex === -1 ? grid.length : breakIndex);

The ternary at the end accounts for the cases when there's only a single item in the grid, and/or a single row of items.

const getNumPerRow = (selector) => {
  const grid = Array.from(document.querySelector(selector).children);
  const baseOffset = grid[0].offsetTop;
  const breakIndex = grid.findIndex(item => item.offsetTop > baseOffset);
  return (breakIndex === -1 ? grid.length : breakIndex);
}
.grid {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  width: 400px;
  background-color: #ddd;
  padding: 10px 0 0 10px;
  margin-top: 5px;
  resize: horizontal;
  overflow: auto;
}

.item {
  width: 50px;
  height: 50px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
<button onclick="alert(getNumPerRow('#grid'))">Get Num Per Row</button>

<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item active"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

But is there an item above or below?

To know if there's an item above or below the active element we need to know 3 parameters:

  • totalItemsInGrid
  • activeIndex
  • numPerRow

For example, in the following structure:

<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item active"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

we have a totalItemsInGrid of 5, the activeIndex has a zero-based index of 2 (it's the 3rd element in the group), and let's say the numPerRow is 3.

We can now determine if there's an item above, below, left, or right of the active item with:

  • isTopRow = activeIndex <= numPerRow - 1
  • isBottomRow = activeIndex >= totalItemsInGid - numPerRow
  • isLeftColumn = activeIndex % numPerRow === 0
  • isRightColumn = activeIndex % numPerRow === numPerRow - 1 || activeIndex === gridNum - 1

If isTopRow is true we cannot move up, and if isBottomRow is true we cannot move down. If isLeftColumn is true we cannot move left, and if isRightColumn if true we cannot move right.

Note: isBottomRow doesn't only check if the active element is on the bottom row, but also checks if there's an element beneath it. In our example above, the active element is not on the bottom row, but doesn't have an item beneath it.

A Working Example

I've worked this into a full example that works with resizing - and made the #grid element resizable so it can be tested in the snippet below.

I've created a function, navigateGrid that accepts three parameters:

  • gridSelector - a DOM selector for the grid element
  • activeClass - the class name of the active element
  • direction - one of up, down, left, or right

This can be used as 'navigateGrid("#grid", "active", "up") with the HTML structure from your question.

The function calculates the number of rows using the offset method, then does the checks to see if the active element can be changed to the up/down/left/right element.

In other words, the function checks if the active element can be moved up/down and left/right. This means:

  • can't go left from the left-most column
  • can't go right from the right-most column
  • can't go up from the top row
  • can't go down from the bottom row, or if the cell below is empty

const navigateGrid = (gridSelector, activeClass, direction) => {
  const grid = document.querySelector(gridSelector);
  const active = grid.querySelector(`.${activeClass}`);
  const activeIndex = Array.from(grid.children).indexOf(active);

  const gridChildren = Array.from(grid.children);
  const gridNum = gridChildren.length;
  const baseOffset = gridChildren[0].offsetTop;
  const breakIndex = gridChildren.findIndex(item => item.offsetTop > baseOffset);
  const numPerRow = (breakIndex === -1 ? gridNum : breakIndex);

  const updateActiveItem = (active, next, activeClass) => {
    active.classList.remove(activeClass);
    next.classList.add(activeClass); 
  }
  
  const isTopRow = activeIndex <= numPerRow - 1;
  const isBottomRow = activeIndex >= gridNum - numPerRow;
  const isLeftColumn = activeIndex % numPerRow === 0;
  const isRightColumn = activeIndex % numPerRow === numPerRow - 1 || activeIndex === gridNum - 1;
  
  switch (direction) {
    case "up":
      if (!isTopRow)
        updateActiveItem(active, gridChildren[activeIndex - numPerRow], activeClass);
      break;
    case "down":
      if (!isBottomRow)
        updateActiveItem(active, gridChildren[activeIndex + numPerRow], activeClass);
      break;  
    case "left":
      if (!isLeftColumn)
        updateActiveItem(active, gridChildren[activeIndex - 1], activeClass);
      break;   
    case "right":
      if (!isRightColumn)
        updateActiveItem(active, gridChildren[activeIndex + 1], activeClass);    
      break;
  }
}
.grid {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  width: 400px;
  background-color: #ddd;
  padding: 10px 0 0 10px;
  margin-top: 5px;
  resize: horizontal;
  overflow: auto;
}

.item {
  width: 50px;
  height: 50px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
<button onClick='navigateGrid("#grid", "active", "up")'>Up</button>
<button onClick='navigateGrid("#grid", "active", "down")'>Down</button>
<button onClick='navigateGrid("#grid", "active", "left")'>Left</button>
<button onClick='navigateGrid("#grid", "active", "right")'>Right</button>

<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item active"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>
like image 178
Brett DeWoody Avatar answered Oct 13 '22 18:10

Brett DeWoody


(For optimal experience better run the interactive snippets on full page)

Calculating number of elements per row

You need to get the width of an element with its margin (eventually border if they are set also) then you need to get the inner width of the container without padding. Having these 2 values you do a simple division to get the number of element per row.

Don't forget to consider the case where you have only one row, so you need to get the minimum value between the total number of elements and the number you get from the division.

//total number of element
var n_t = document.querySelectorAll('.item').length;
//width of an element
var w = parseInt(document.querySelector('.item').offsetWidth);
//full width of element with margin
var m = document.querySelector('.item').currentStyle || window.getComputedStyle(document.querySelector('.item'));
w = w + parseInt(m.marginLeft) + parseInt(m.marginRight);
//width of container
var w_c = parseInt(document.querySelector('.grid').offsetWidth);
//padding of container
var c = document.querySelector('.grid').currentStyle || window.getComputedStyle(document.querySelector('.grid'));
var p_c = parseInt(c.paddingLeft) + parseInt(c.paddingRight);
//nb element per row
var nb = Math.min(parseInt((w_c - p_c) / w),n_t);
console.log(nb);


window.addEventListener('resize', function(event){
   //only the width of container will change
   w_c = parseInt(document.querySelector('.grid').offsetWidth);
   nb = Math.min(parseInt((w_c - p_c) / w),n_t);
   console.log(nb);
});
.grid {
  display: flex;
  flex-wrap: wrap;
  resize:horizontal;
  align-content: flex-start;
  background-color: #ddd;
  padding: 10px 0 0 10px;
}

.item {
  width: 80px;
  height: 80px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item active"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

Here is a jQuery version of the same logic with less of code:

//total number of element
var n_t = $('.item').length;
//full width of element with margin
var w = $('.item').outerWidth(true);
//width of container without padding
var w_c = $('.grid').width();
//nb element per row
var nb = Math.min(parseInt(w_c / w),n_t);
console.log(nb);

window.addEventListener('resize', function(event){
   //only the width of container will change
   w_c = $('.grid').width();
   nb = Math.min(parseInt(w_c / w),n_t);
   console.log(nb);
});
.grid {
  display: flex;
  flex-wrap: wrap;
  resize:horizontal;
  align-content: flex-start;
  background-color: #ddd;
  padding: 10px 0 0 10px;
}

.item {
  width: 80px;
  height: 80px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item active"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

And here is a demonstration of the interactive grid:

var all = document.querySelectorAll('.item');
var n_t = all.length;
var current = 0;
all[current].classList.add('active');

var w = parseInt(document.querySelector('.item').offsetWidth);
var m = document.querySelector('.item').currentStyle || window.getComputedStyle(document.querySelector('.item'));
w = w + parseInt(m.marginLeft) + parseInt(m.marginRight);
var w_c = parseInt(document.querySelector('.grid').offsetWidth);
var c = document.querySelector('.grid').currentStyle || window.getComputedStyle(document.querySelector('.grid'));
var p_c = parseInt(c.paddingLeft) + parseInt(c.paddingRight);
var nb = Math.min(parseInt((w_c - p_c) / w),n_t);

window.addEventListener('resize', function(e){
   w_c = parseInt(document.querySelector('.grid').offsetWidth);
   nb = Math.min(parseInt((w_c - p_c) / w),n_t);
});

document.addEventListener('keydown',function (e) {
    e = e || window.event;
    if (e.keyCode == '38') {
        if(current - nb>=0) {
          all[current].classList.remove('active');
          current-=nb;
          all[current].classList.add('active');
       }
    }
    else if (e.keyCode == '40') {
        if(current + nb<n_t) {
          all[current].classList.remove('active');
          current+=nb;
          all[current].classList.add('active');
       }
    }
    else if (e.keyCode == '37') {
       if(current>0) {
          all[current].classList.remove('active');
          current--;
          all[current].classList.add('active');
       }
    }
    else if (e.keyCode == '39') {
       if(current<n_t-1) {
          all[current].classList.remove('active');
          current++;
          all[current].classList.add('active');
       }
          
    }
});
.grid {
  display: flex;
  flex-wrap: wrap;
  resize:horizontal;
  align-content: flex-start;
  background-color: #ddd;
  padding: 10px 0 0 10px;
}

.item {
  width: 80px;
  height: 80px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

Another Idea

We can also consider another way to navigate inside the grid without the need of the number of element per row. The idea is to rely on the function elementFromPoint(x,y).

The logic is as follows: We are inside an active element and we have its (x,y) position. By pressing a key we will increase/decrease these values and we use the above function to get the new element using the new (x,y). We test if we get a valid element and if this element is an item (contains item class). In this case we remove active from the previous one and we add it to the new one.

Here is a example where I only consider an inside navigation. When we reach left/right boundary of the container we will not get to previous/next line:

var a = document.querySelector('.item');
a.classList.add('active');

var off = a.getBoundingClientRect();
/* I get the center position to avoid any potential issue with boundaries*/
var y = off.top + 40; 
var x = off.left + 40;

document.addEventListener('keydown', function(e) {
  e = e || window.event;
  if (e.keyCode == '38') {
    var elem = document.elementFromPoint(x, y - 90 /* width + both margin*/);
    if (elem &&
      elem.classList.contains('item')) {
      document.querySelector('.active').classList.remove('active');
      elem.classList.add('active');
      y -= 90;
    }
  } else if (e.keyCode == '40') {
    var elem = document.elementFromPoint(x, y + 90);
    if (elem &&
      elem.classList.contains('item')) {
      document.querySelector('.active').classList.remove('active');
      elem.classList.add('active');
      y += 90;
    }
  } else if (e.keyCode == '37') {
    var elem = document.elementFromPoint(x - 90, y);
    if (elem &&
      elem.classList.contains('item')) {
      document.querySelector('.active').classList.remove('active');
      elem.classList.add('active');
      x -= 90;
    }
  } else if (e.keyCode == '39') {
    var elem = document.elementFromPoint(x + 90, y);
    if (elem &&
      elem.classList.contains('item')) {
      document.querySelector('.active').classList.remove('active');
      elem.classList.add('active');
      x += 90;
    }
  }
});

window.addEventListener('resize', function(e) {
  var off = document.querySelector('.active').getBoundingClientRect();
  y = off.top + 40;
  x = off.left + 40;
});
.grid {
  display: flex;
  flex-wrap: wrap;
  resize: horizontal;
  align-content: flex-start;
  background-color: #ddd;
  padding: 10px 0 0 10px;
}

.item {
  width: 80px;
  height: 80px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
<div id="grid" class="grid">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

As you may notice in this method, we don't need any information about the container, the screen size, the number of element, etc. The only needed information is the dimension of a single item. We also need a small code to rectify the position of the active element on window resize.


Bonus

Here is another fancy idea if you want to have a visually active element without the need of adding a class or getting it with JS. The idea is to use background on the container to create a black box behind the active element.

By the way, this method has 2 drawbacks:

  1. Not easy to deal with the last line if it's not full of element as we may have the black box behind nothing
  2. We have to consider the space left after the last element of each row to avoid having a strange position of the black box.

Here is a simplified code with a fixed height/width container:

var grid = document.querySelector('.grid');

document.addEventListener('keydown', function(e) {
  e = e || window.event;
  if (e.keyCode == '38') {
    var y = parseInt(grid.style.backgroundPositionY);
    y= (y-90 + 270)%270;
    grid.style.backgroundPositionY=y+"px";
  } else if (e.keyCode == '40') {
    var y = parseInt(grid.style.backgroundPositionY);
    y= (y+90)%270;
    grid.style.backgroundPositionY=y+"px";
  } else if (e.keyCode == '37') {
    var x = parseInt(grid.style.backgroundPositionX);
    x= (x-90 + 270)%270;
    grid.style.backgroundPositionX=x+"px";
  } else if (e.keyCode == '39') {
    var x = parseInt(grid.style.backgroundPositionX);
    x= (x+90)%270;
    grid.style.backgroundPositionX=x+"px";
  }
});
.grid {
  display: flex;
  flex-wrap: wrap;
  width:270px;
  resize: horizontal;
  align-content: flex-start;
  background-color: #ddd;
  padding: 10px 0 0 10px;
  background-image:linear-gradient(#000,#000);
  background-size:90px 90px;
  background-repeat:no-repeat;
}

.item {
  width: 80px;
  height: 80px;
  background-color: red;
  margin: 0 10px 10px 0;
}
<div id="grid" class="grid" style="background-position:5px 5px;">
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>

As we can see the code is pretty simple so it can be suitable for such situation where almost all the values are known and fixed.

like image 26
Temani Afif Avatar answered Oct 13 '22 19:10

Temani Afif


The only way to move around up and down that arises less unwanted complication to my knowledge is having the count of boxes per row and changing the indexes. The only problem is you need to calculate the boxcount on both window load and resize event.

var boxPerRow=0;
function calculateBoxPerRow(){}
window.onload = calculateBoxPerRow; 
window.onresize = calculateBoxPerRow;

Now if you want a very simple way to get the number of boxes in a row without even caring about the size of neither the container nor the boxes, forget margins and paddings, you can check how many boxes are aligned with the first box comparing the offsetTop property.

The HTMLElement.offsetTop read-only property returns the distance of the current element relative to the top of the offsetParent node. [source: developer.mozilla.orgl]

You can implement it like below:

function calculateBoxPerRow(){
    var boxes = document.querySelectorAll('.item');
    if (boxes.length > 1) {
‎       var i = 0, total = boxes.length, firstOffset = boxes[0].offsetTop;
‎       while (++i < total && boxes[i].offsetTop == firstOffset);
‎       boxPerRow = i;
‎   }
}

Full working example:

(function() {
  var boxes = document.querySelectorAll('.item');
  var boxPerRow = 0, currentBoxIndex = 0;

  function calculateBoxPerRow() {
    if (boxes.length > 1) {
      var i = 0,
        total = boxes.length,
        firstOffset = boxes[0].offsetTop;
      while (++i < total && boxes[i].offsetTop == firstOffset);
      boxPerRow = i;
    }
  }
  window.onload = calculateBoxPerRow;
  window.onresize = calculateBoxPerRow;

  function focusBox(index) {
    if (index >= 0 && index < boxes.length) {
      if (currentBoxIndex > -1) boxes[currentBoxIndex].classList.remove('active');
      boxes[index].classList.add('active');
      currentBoxIndex = index;
    }
  }
  document.body.addEventListener("keyup", function(event) {
    switch (event.keyCode) {
      case 37:
        focusBox(currentBoxIndex - 1);
        break;
      case 39:
        focusBox(currentBoxIndex + 1);
        break;
      case 38:
        focusBox(currentBoxIndex - boxPerRow);
        break;
      case 40:
        focusBox(currentBoxIndex + boxPerRow);
        break;
    }
  });
})();
.grid {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  width: 50%;
  height: 200px;
  background-color: #ddd;
  padding: 10px 0 0 10px;
}

.item {
  width: 50px;
  height: 50px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
<div>[You need to click on this page so that it can recieve the arrow keys]</div>
<div id="grid" class="grid">
  <div class="item active"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
  <div class="item"></div>
</div>
like image 10
Munim Munna Avatar answered Oct 13 '22 19:10

Munim Munna


To support moving up, down, left, and right, you don't need to know how many boxes there are in a row, you just need to calculate if there is a box above, below, left, or right of the active box.

Moving left and right is simple as you've noticed - just check if the active box has a previousSiblingElement or nextSiblingElement. For up and down, you can use the current active box as an anchor point and compare it to the other box's getBoundingClientRect()s, a DOM method which returns the geomoetry of an element relative to the browser viewport.

When trying to move up, start at the anchor and count down through the items towards 0. When moving down, start at the anchor and count until the end of the number of items. This is because when moving up, we only care about boxes before the active box, and when going down we only care about boxes after it. All we need to look for is a box that has the same left position with a higher or lower top position.

Below is an example which listens for a keydown event on window and will move the active state according to which arrow key was pressed. It could definitely be made more DRY, but I've divided up the the four cases so you can see the exact logic in each. You can hold the arrow keys down so the box moves continuously and you can see it's very performant. And I've updated your JSBin with my solution here:http://jsbin.com/senigudoqu/1/edit?html,css,js,output

const items = document.querySelectorAll('.item');

let activeItem = document.querySelector('.item.active');

function updateActiveItem(event) {
  let index;
  let rect1;
  let rect2;

  switch (event.key) {
    case 'ArrowDown':
      index = Array.prototype.indexOf.call(items, activeItem);
      rect1 = activeItem.getBoundingClientRect();

      for (let i = index; i < items.length; i++) {
        rect2 = items[i].getBoundingClientRect();

        if (rect1.x === rect2.x && rect1.y < rect2.y) {
          items[i].classList.add('active');
          activeItem.classList.remove('active');
          activeItem = items[i];
          return;
        }
      }
      break;

    case 'ArrowUp':
      index = Array.prototype.indexOf.call(items, activeItem);
      rect1 = activeItem.getBoundingClientRect();

      for (let i = index; i >= 0; i--) {
        rect2 = items[i].getBoundingClientRect();

        if (rect1.x === rect2.x && rect1.y > rect2.y) {
          items[i].classList.add('active');
          activeItem.classList.remove('active');
          activeItem = items[i];
          return;
        }
      }
      break;

    case 'ArrowLeft':
      let prev = activeItem.previousElementSibling;

      if (prev) {
        prev.classList.add('active');
        activeItem.classList.remove('active');
        activeItem = prev;
      }
      break;

    case 'ArrowRight':
      let next = activeItem.nextElementSibling;

      if (next) {
        next.classList.add('active');
        activeItem.classList.remove('active');
        activeItem = next;
      }
      break;

    default:
      return;
  }
}

window.addEventListener('keydown', updateActiveItem);
.grid {
  display: flex;
  flex-wrap: wrap;
  align-content: flex-start;
  background-color: #ddd;
  padding: 10px 0 0 10px;
}

.item {
  width: 50px;
  height: 50px;
  background-color: red;
  margin: 0 10px 10px 0;
}

.active.item {
  outline: 5px solid black;
}
  <div id="grid" class="grid">
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item active"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
    <div class="item"></div>
  </div>
like image 4
skyline3000 Avatar answered Oct 13 '22 20:10

skyline3000