A grid is implemented using the CSS flexbox. Example:
The number of rows in this example is 4 because I fixed the container width for demo purposes. But, in
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.
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.
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: #ffffd;
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;
}
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:
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.
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 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:
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: #ffffd;
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;
}