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:
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
– theouterWidth
of a single element includingborder
,padding
andmargin
gridWidth
– theinnerWidth
of the grid, excludingborder
,padding
andmargin
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 elementactiveClass
– the class name of the active elementdirection
– one ofup
,down
,left
, orright
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>