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
I know this is not exactly what OP is asking for, but I wanted to show a possible alternative (depends on use-case).
Instead of using CSS flexbox, there is also the more recent CSS grid which actually features columns and rows. Thus by converting the structure to a grid and using some JS to listen to the key buttons being pressed, the active item can be moved (see incomplete working example below).
var x = 1, y = 1;
document.addEventListener('keydown', function(event) {
const key = event.key;
// "ArrowRight", "ArrowLeft", "ArrowUp", or "ArrowDown"
console.log(key);
if (key == "ArrowRight") {
x++;
}
if (key == "ArrowLeft") {
x--;
if (x < 1) {
x = 1;
}
}
if (key == "ArrowUp") {
y--;
if (y < 1) {
y = 1;
}
}
if (key == "ArrowDown") {
y++;
}
document.querySelector('.active').style.gridColumnStart = x;
document.querySelector('.active').style.gridRowStart = y;
});
.grid {
display: grid;
grid-template-columns: repeat(auto-fill,50px);
grid-template-rows: auto;
grid-gap: 10px;
width: 250px;
height: 200px;
background-color: #ffffd;
padding: 10px;
}
.item {
width: 50px;
height: 50px;
background-color: red;
margin: 0 10px 10px 0;
display: flex;
justify-content: center;
align-items: center;
}
.active {
outline: 5px solid black;
grid-column-start: 1;
grid-column-end: span 1;
grid-row-start: 1;
grid-row-end: span 1;
}
<div id="grid" class="grid">
<div class="item active">A1</div>
<div class="item">A2</div>
<div class="item">A3</div>
<div class="item">A4</div>
<div class="item">B1</div>
<div class="item">B2</div>
<div class="item">B3</div>
<div class="item">B4</div>
<div class="item">C1</div>
<div class="item">C2</div>
</div>
However, as stated above, this solution has flaws. For once, the active item is actually a grid item by itself and moved along the grid with the other elements flowing around it. Secondly, similar to flexbox model, there are currently no CSS selectors to target an item based upon its grid position.
However, since we are using javascript anyway, you could loop through all grid items and get the CSS Grid properties. If they match the current coordinates, you have your target element. Sadly, this would only work if each element is placed, using grid-column-start: auto
for elements doesn't help. Even window.getComputedStyle()
will only return auto
;
This example assumes movement ends at the bounds. Also, if moving from the second to last row down to the last row, but there are fewer columns in the last row, it will move to the last column of the last row instead.
This solution keeps track of row/columns and uses a grid object to keep track of where the elements are.
var items = document.querySelectorAll(".item");
var grid = {}; // keys: row, values: index of div in items variable
var row, col, numRows;
// called only onload and onresize
function populateGrid() {
grid = {};
var prevTop = -99;
var row = -1;
for(idx in items) {
if(isNaN(idx)) continue;
if(items[idx].offsetTop !== prevTop) {
prevTop = items[idx].offsetTop;
row++;
grid[row] = [];
}
grid[row].push(idx);
}
setActiveRowAndCol();
numRows = Object.keys(grid).length
}
// changes active state from one element to another
function updateActiveState(oldElem, newElem) {
oldElem.classList.remove('active');
newElem.classList.add('active');
}
// only called from populateGrid to get new row/col of active element (in case of wrap)
function setActiveRowAndCol() {
var activeIdx = -1;
for(var idx in items) {
if(items[idx].className == "item active")
activeIdx = idx;
}
for(var key in grid) {
var gridIdx = grid[key].indexOf(activeIdx);
if(gridIdx > -1) {
row = key;
col = gridIdx;
}
}
}
function moveUp() {
if(0 < row) {
var oldElem = items[grid[row][col]];
row--;
var newElem = items[grid[row][col]];
updateActiveState(oldElem, newElem);
}
}
function moveDown() {
if(row < numRows - 1) {
var oldElem = items[grid[row][col]];
row++;
var rowLength = grid[row].length
var newElem;
if(rowLength-1 < col) {
newElem = items[grid[row][rowLength-1]]
col = rowLength-1;
} else {
newElem = items[grid[row][col]];
}
updateActiveState(oldElem, newElem);
}
}
function moveLeft() {
if(0 < col) {
var oldElem = items[grid[row][col]];
col--;
var newElem = items[grid[row][col]];
updateActiveState(oldElem, newElem);
}
}
function moveRight() {
if(col < grid[row].length - 1) {
var oldElem = items[grid[row][col]];
col++;
var newElem = items[grid[row][col]];
updateActiveState(oldElem, newElem);
}
}
document.onload = populateGrid();
window.addEventListener("resize", populateGrid);
document.addEventListener('keydown', function(e) {
e = e || window.event;
if (e.keyCode == '38') {
moveUp();
} else if (e.keyCode == '40') {
moveDown();
} else if (e.keyCode == '37') {
moveLeft();
} else if (e.keyCode == '39') {
moveRight();
}
});
.grid {
display: flex;
flex-wrap: wrap;
resize: horizontal;
align-content: flex-start;
background-color: #ffffd;
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 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>
This example assumes movement ends at the bounds. Also, if moving from the second to last row down to the last row, but there are fewer columns in the last row, it will move to the last column of the last row instead.
This solution keeps track of row/columns and uses a grid object to keep track of where the elements are. The positions will be updated in the grid object when the page is resized.
(you can see the wrapping update in action in full-screen mode)
var items = document.querySelectorAll(".item");
var grid = {}; // keys: row, values: index of div in items variable
var row, col, numRows;
// called only onload and onresize
function populateGrid() {
grid = {};
var prevTop = -99;
var row = -1;
for(idx in items) {
if(isNaN(idx)) continue;
if(items[idx].offsetTop !== prevTop) {
prevTop = items[idx].offsetTop;
row++;
grid[row] = [];
}
grid[row].push(idx);
}
setActiveRowAndCol();
numRows = Object.keys(grid).length
}
// changes active state from one element to another
function updateActiveState(oldElem, newElem) {
oldElem.classList.remove('active');
newElem.classList.add('active');
}
// only called from populateGrid to get new row/col of active element (in case of wrap)
function setActiveRowAndCol() {
var activeIdx = -1;
for(var idx in items) {
if(items[idx].className == "item active")
activeIdx = idx;
}
for(var key in grid) {
var gridIdx = grid[key].indexOf(activeIdx);
if(gridIdx > -1) {
row = key;
col = gridIdx;
}
}
}
function moveUp() {
if(0 < row) {
var oldElem = items[grid[row][col]];
row--;
var newElem = items[grid[row][col]];
updateActiveState(oldElem, newElem);
}
}
function moveDown() {
if(row < numRows - 1) {
var oldElem = items[grid[row][col]];
row++;
var rowLength = grid[row].length
var newElem;
if(rowLength-1 < col) {
newElem = items[grid[row][rowLength-1]]
col = rowLength-1;
} else {
newElem = items[grid[row][col]];
}
updateActiveState(oldElem, newElem);
}
}
function moveLeft() {
if(0 < col) {
var oldElem = items[grid[row][col]];
col--;
var newElem = items[grid[row][col]];
updateActiveState(oldElem, newElem);
}
}
function moveRight() {
if(col < grid[row].length - 1) {
var oldElem = items[grid[row][col]];
col++;
var newElem = items[grid[row][col]];
updateActiveState(oldElem, newElem);
}
}
document.onload = populateGrid();
window.addEventListener("resize", populateGrid);
document.addEventListener('keydown', function(e) {
e = e || window.event;
if (e.keyCode == '38') {
moveUp();
} else if (e.keyCode == '40') {
moveDown();
} else if (e.keyCode == '37') {
moveLeft();
} else if (e.keyCode == '39') {
moveRight();
}
});
.grid {
display: flex;
flex-wrap: wrap;
resize: horizontal;
align-content: flex-start;
background-color: #ffffd;
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 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>
You could use the Array.prototype.filter() to do this quite neatly. To get the amount of items in a row use this function. Pass in the CSS selector that you want to use (in this case .item). Once you have the row size the arrow navigation is easy.
function getRowSize( cssSelector ) {
var firstTop = document.querySelector( cssSelector ).offsetTop;
// Sets rowArray to be an array of the nodes (divs) in the 1st row.
var rowArray = Array.prototype.filter.call(document.querySelectorAll( cssSelector ), function(element){
if( element.offsetTop == firstTop ) return element;
});
// Return the amount of items in a row.
return rowArray.length;
}
Examples
CodePen demo: https://codepen.io/gtlitc/pen/EExXQE
Interactive demo that displays the row size and move amounts. http://www.smallblue.net/demo/49043684/
Explanation
Firstly the function sets a variable firstTop
to be the offsetTop
of the very first node.
Next the function builds an array rowArray
of nodes in the first row (if up and down navigation is possible the first row will always be a full length row).
This is done by calling (borrowing) the filter function from the Array Prototype. We cant simply call the filter function on the node list that is returned by the QSA (query selector all) because browsers return node lists instead of arrays and node lists are not proper arrays.
The if statement then simply filters all of the nodes and only returns the ones that have the same offsetTop
as the first node. i.e all of the nodes in the first row.
We now have an array from which we can determine the length of a row.
I have omitted the implementation of the DOM traversal as this is simple using either pure Javascript or Jquery etc and was not part of the OPs question. I would only note that it is important to test if the element you intend to move to exists before moving there.
This function will work with any layout technique. Flexbox, float, CSS grid, whatever the future holds.
References
Why does document.querySelectorAll return a StaticNodeList rather than a real Array?
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
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: #ffffd;
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>
If you're using Jquery and are confident that your grid objects are vertically aligned, this could do the trick..
I didnt test it, but it should work (by counting the columns)
function countColumns(){
var objects = $(".grid-object"); // choose a unique class name here
var columns = []
for(var i=0;i<objects.length;i++){
var pos = $(objects[i]).position().left
if(columns.indexOf(pos) < 1) columns.push(pos);
}
return columns.length
}