web-dev-qa-db-fra.com

Comment calculer le nombre d'éléments de la flexbox dans une ligne?

Une grille est implémentée à l'aide de la flexbox CSS. Exemple:

 enter image description here

Le nombre de lignes dans cet exemple est 4 car j'ai corrigé la largeur du conteneur à des fins de démonstration. Mais, en réalité, il peut changer en fonction de la largeur du conteneur (par exemple, si l'utilisateur redimensionne la fenêtre). Essayez de redimensionner la fenêtre de sortie dans cet exemple pour avoir une idée.

Il y a toujours un élément actif, marqué par la bordure noire.

À l'aide de JavaScript, j'autorise les utilisateurs à accéder à l'élément précédent/suivant à l'aide de la flèche gauche/droite. Dans mon implémentation, je viens de diminuer/augmenter l'indice de l'élément actif de 1.

J'aimerais maintenant permettre aux utilisateurs de naviguer vers le haut ou le bas. Pour cela, il me suffit de diminuer/augmenter l'index de l'élément actif de <amount of items in a row>. Mais comment calculer ce nombre, étant donné qu'il dépend de la largeur du conteneur? Existe-t-il un meilleur moyen de mettre en œuvre la fonctionnalité de montée/descente?

.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>

45
Misha Moroshko

La question est légèrement plus complexe que de savoir combien d'éléments sont à la suite.

En fin de compte, nous voulons savoir s’il existe un élément au-dessus, au-dessous, à gauche et à droite de l’élément actif. Et cela doit tenir compte des cas où la rangée du bas est incomplète. Par exemple, dans le cas ci-dessous, l'élément actif n'a aucun élément au-dessus, en dessous ou à droite:

 enter image description here

Toutefois, afin de déterminer s’il existe un élément situé au-dessus/au-dessous/à gauche/à droite de l’élément actif, nous devons savoir combien d’éléments sont dans une rangée.

Trouvez le nombre d'articles par rangée

Pour obtenir le nombre d'éléments par ligne, nous avons besoin de:

  • itemWidth - la outerWidth d'un seul élément, y compris border, padding et margin 
  • gridWidth - la innerWidth de la grille, à l'exclusion de border, padding et margin

Pour calculer ces deux valeurs avec JavaScript, nous pouvons utiliser:

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));

Ensuite, nous pouvons calculer le nombre d'éléments par ligne en utilisant:

const numPerRow = Math.floor(gridWidth / itemWidth)

Remarque: cela ne fonctionnera que pour les éléments de taille uniforme et uniquement si la variable margin est définie en unités px.

Une approche beaucoup, beaucoup, beaucoup plus simple

Traiter avec toutes ces largeurs et ces rembourrages, marges et bordures est vraiment déroutant. Il y a une solution beaucoup, beaucoup, beaucoup plus simple.

Il suffit de trouver l'index de l'élément de grille dont la propriété offsetTop est supérieure à celle du premier élément de grille 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);

Le compte ternaire à la fin des comptes pour les cas où il n'y a qu'un seul élément dans la grille et/ou une seule ligne d'éléments.

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>

Mais y a-t-il un objet au-dessus ou au-dessous?

Pour savoir s'il y a un élément au-dessus ou au-dessous de l'élément actif, nous devons connaître 3 paramètres:

  • totalItemsInGrid
  • activeIndex
  • numPerRow

Par exemple, dans la structure suivante:

<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>

nous avons une totalItemsInGrid de 5, la activeIndex a un index de base zéro de 2 (c'est le 3ème élément du groupe), et disons que la numPerRow est 3.

Nous pouvons maintenant déterminer s'il existe un élément au-dessus, au-dessous, à gauche ou à droite de l'élément actif avec:

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

Si isTopRow est true nous ne pouvons pas monter, et si isBottomRow est true nous ne pouvons pas descendre. Si isLeftColumn est true nous ne pouvons pas nous déplacer à gauche, et si isRightColumn si true nous ne pouvons pas nous déplacer à droite.

Note: isBottomRow ne vérifie pas seulement si l'élément actif est sur la dernière ligne, mais également s'il y a un élément en dessous. Dans notre exemple ci-dessus, l'élément actif est not sur la dernière ligne, mais aucun élément ne se trouve en dessous.

Un exemple de travail

J'ai intégré cela à un exemple complet qui fonctionne avec le redimensionnement - et rendu l'élément #grid redimensionnable afin qu'il puisse être testé dans l'extrait de code ci-dessous. 

J'ai créé une fonction, navigateGrid, qui accepte trois paramètres:

  • gridSelector - un sélecteur DOM pour l'élément de grille
  • activeClass - le nom de classe de l'élément actif
  • direction - un des up, down, left ou right

Ceci peut être utilisé comme 'navigateGrid("#grid", "active", "up") avec la structure HTML de votre question.

La fonction calcule le nombre de lignes à l'aide de la méthode offset, puis effectue les vérifications pour voir si l'élément active peut être remplacé par l'élément haut/bas/gauche/droite.

En d'autres termes, la fonction vérifie si l'élément actif peut être déplacé vers le haut et le bas et à gauche/à droite. Ça signifie:

  • ne peut pas aller à gauche de la colonne la plus à gauche
  • ne peut pas aller à droite de la colonne la plus à droite
  • ne peut pas monter de la rangée du haut
  • ne peut pas descendre de la ligne du bas, ou si la cellule ci-dessous est vide 

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>

23
Brett DeWoody

(Pour une expérience optimale, exécutez les extraits interactifs sur toute la page)}

Calculer le nombre d'éléments par ligne

Vous devez obtenir la largeur d'un élément avec sa marge _ ​​(éventuellement si les bordures sont également définies)}, vous devez alors obtenir la largeur intérieure du conteneur sans remplissage. Avec ces 2 valeurs, vous effectuez une division simple pour obtenir le nombre d'éléments par ligne.

N'oubliez pas de prendre en compte le cas où vous n'avez qu'une seule ligne. Vous devez donc obtenir la valeur minimale entre le nombre total d'éléments et le nombre obtenu de la 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>

Voici une version jQuery de la même logique avec moins de 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>


Et voici une démonstration de la grille interactive:

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>


Une autre idée

Nous pouvons également envisager une autre façon de naviguer à l'intérieur de la grille sans avoir besoin du nombre d'éléments par ligne. L'idée est de s'appuyer sur la fonction elementFromPoint(x,y) .

La logique est la suivante: Nous sommes dans un élément actif et nous avons sa position (x,y). En appuyant sur une touche, nous augmentons/diminuons ces valeurs et nous utilisons la fonction ci-dessus pour obtenir le nouvel élément à l'aide du nouveau (x,y). Nous testons si nous obtenons un élément valide et si cet élément est un élément (contient item classe)}. Dans ce cas, nous supprimons active de la précédente et nous l'ajoutons à la nouvelle.

Voici un exemple où je considère uniquement une navigation inside. Lorsque nous atteignons la limite gauche/droite du conteneur, nous n'atteindrons pas la ligne précédente/suivante:

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>

Comme vous le remarquerez peut-être dans cette méthode, nous n’avons besoin d’aucune information sur le conteneur, la taille de l’écran, le nombre d’éléments, etc. La seule information nécessaire est la dimension d’un seul élément. Nous avons également besoin d’un petit code pour rectifier la position de l’élément actif lors du redimensionnement de la fenêtre.


Prime

Voici une autre idée fantaisie si vous voulez avoir un élément actif visuellement sans avoir besoin d'ajouter une classe ou de l'obtenir avec JS. L'idée est d'utiliser l'arrière-plan du conteneur pour créer une boîte noire derrière l'élément actif.

À propos, cette méthode a 2 inconvénients:

  1. Pas facile de traiter la dernière ligne si elle n’est pas pleine d’éléments, car on risque d’avoir la boîte noire derrière rien
  2. Nous devons considérer l'espace laissé après le dernier élément de chaque ligne pour éviter d'avoir une position étrange de la boîte noire.

Voici un code simplifié avec un conteneur hauteur/largeur fixe:

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>

Comme nous pouvons le constater, le code est assez simple et peut donc convenir à une telle situation où presque toutes les valeurs sont connues et fixes.

22
Temani Afif

À ma connaissance, le seul moyen de se déplacer de haut en bas qui pose moins de problèmes indésirables est d'avoir le nombre de boîtes par ligne et de changer les index. Le seul problème est que vous devez calculer le nombre de boîtes à la fois lors du chargement et du redimensionnement de la fenêtre.

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

Maintenant, si vous voulez un moyen très simple d’obtenir le nombre de cases dans une rangée sans vous soucier de la taille ni du conteneur ni des cases, oublier les marges et les paddings, vous pouvez vérifier le nombre de cases sont alignés sur la première case comparant la propriété offsetTop.

La propriété en lecture seule HTMLElement.offsetTop renvoie la distance de l'élément actuel par rapport au sommet du nœud offsetParent. [source: developer.mozilla.orgl ]

Vous pouvez l'implémenter comme ci-dessous: 

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;
‎   }
}

Exemple de travail complet:

(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>

7
Munim Munna

Pour prendre en charge le déplacement vers le haut, le bas, la gauche et la droite, vous n'avez pas besoin de savoir combien de cases se trouvent dans une rangée, vous devez simplement calculer s'il existe une case au-dessus, en dessous, à gauche ou à droite de la case active .

Déplacer vers la gauche et la droite est simple, comme vous l'avez remarqué - il suffit de vérifier si la case active a previousSiblingElement ou nextSiblingElement. Pour monter et descendre, vous pouvez utiliser la zone active actuelle comme point d'ancrage et la comparer à la fonction getBoundingClientRect()s de l'autre boîte, une méthode DOM qui renvoie la géométrie d'un élément par rapport à la fenêtre de visualisation du navigateur.

Lorsque vous essayez de vous déplacer vers le haut, commencez à l'ancre et comptez jusqu'à 0 dans les éléments. Pour descendre, commencez à l'ancre et comptez jusqu'à la fin du nombre d'éléments. En effet, lorsqu’on monte, on ne s’intéresse qu’aux boîtes avant la boîte active, et lorsqu’on descend, on ne s’intéresse qu’aux boîtes après. Tout ce que nous devons rechercher est une boîte qui a la même position gauche avec une position haute plus haute ou plus basse.

Vous trouverez ci-dessous un exemple qui écoute un événement keydown sur window et déplace l'état actif en fonction de la touche de direction sur laquelle vous avez appuyé. Cela pourrait certainement être rendu plus sec, mais j'ai divisé les quatre cas afin que vous puissiez voir la logique exacte dans chacun. Vous pouvez maintenir les touches fléchées enfoncées pour que la boîte continue à se déplacer et que vous puissiez voir qu'elle est très performante. Et j'ai mis à jour votre solution JSBin avec ma solution ici: 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>

2
skyline3000

Bien que vous puissiez calculer l'élément que vous recherchez, je vous suggère de rechercher l'élément ci-dessous. L'avantage de ceci est que cela fonctionnerait même si vos éléments n'avaient pas la même largeur.

Alors réfléchissons aux attributs de l'élément ci-dessous. Il s’agit essentiellement du premier élément avec une offsetTop supérieure et le même offsetLeft. Vous pouvez faire quelque chose comme ceci pour trouver l'élément ontop:

const active = document.querySelector('.item.active');
const all = [...document.querySelectorAll('.item')]
const below = all
  .filter(c => c.offsetTop > active.offsetTop)
  .find(c => c.offsetLeft >= active.offsetLeft)
const ontop = [...all].reverse()
  .filter(c => c.offsetTop < active.offsetTop)
  .find(c => c.offsetLeft >= active.offsetLeft)
1
Lux

Cet exemple suppose que le mouvement se termine aux limites. De même, si vous passez de la deuxième à la dernière ligne à la dernière, mais que le nombre de colonnes dans la dernière ligne est moins élevé, la dernière colonne passera à la dernière.

Cette solution assure le suivi des lignes/colonnes et utilise un objet de la grille pour savoir où se trouvent les éléments. Les positions seront mises à jour dans l'objet de la grille lorsque la page sera redimensionnée.

(vous pouvez voir la mise à jour enveloppante en action en mode plein écran)

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: #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 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>

1
Emil

Si vous utilisez Jquery et que vous êtes sûr que vos objets de la grille sont alignés verticalement, cela pourrait faire l'affaire. 

Je ne l'ai pas testé, mais cela devrait fonctionner (en comptant les colonnes)

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
}
0
JeanMGirard

Vous pouvez utiliser Array.prototype.filter () pour le faire parfaitement . Pour obtenir le nombre d’éléments dans une ligne, utilisez cette fonction . Passez le sélecteur CSS que vous souhaitez utiliser (dans ce cas .item) . Une fois que vous avez la taille de la ligne, la navigation par flèche est facile. 

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;
}

Exemples

Démo CodePen: https://codepen.io/gtlitc/pen/EExXQE

Démo interactive qui affiche la taille de la ligne et le montant des déplacements . http://www.smallblue.net/demo/49043684/

Explication

Premièrement, la fonction définit une variable firstTop comme étant la offsetTop du tout premier nœud.

Ensuite, la fonction crée un tableau rowArray de nœuds dans la première ligne (si la navigation en haut et en bas est possible, la première ligne sera toujours une ligne de longueur complète).

Ceci est fait en appelant (empruntant) la fonction de filtre du prototype de matrice. Nous ne pouvons pas simplement appeler la fonction de filtrage sur la liste de nœuds renvoyée par le QSA (requête de sélection de tous) car les navigateurs renvoient des listes de nœuds au lieu de tableaux et que les listes de nœuds ne sont pas correctes.

L'instruction if filtre alors simplement tous les noeuds et ne renvoie que ceux qui ont le même offsetTop que le premier noeud. c'est-à-dire tous les nœuds de la première rangée.

Nous avons maintenant un tableau à partir duquel nous pouvons déterminer la longueur d'une ligne.

J'ai omis la mise en œuvre de la traversée du DOM, car elle est simple, à l'aide de Javascript pur ou de Jquery, etc., et ne fait pas partie de la question des OP. Je ferais seulement remarquer qu'il est important de vérifier si l'élément que vous avez l'intention de déplacer existe avant de le faire.

Cette fonction fonctionnera avec n’importe quelle technique de mise en page . Flexbox, float, grille CSS, quelle que soit la situation future.

Références

Pourquoi document.querySelectorAll renvoie-t-il un StaticNodeList plutôt qu'un véritable tableau?

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter

0

Je sais que ce n'est pas exactement ce que demande OP, mais je voulais montrer une alternative possible (dépend du cas d'utilisation).

Au lieu d'utiliser CSS flexbox, il y a aussi la grille CSS la plus récente qui contient en réalité des colonnes et des lignes. Ainsi, en convertissant la structure en une grille et en utilisant du JS pour écouter les touches enfoncées, l'élément actif peut être déplacé (voir l'exemple de travail incomplet ci-dessous). 

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: #ddd;
  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>

Cependant, comme indiqué ci-dessus, cette solution présente des défauts. Pour une fois, l'élément actif est en fait un élément de grille à part entière et est déplacé le long de la grille avec les autres éléments circulant autour de lui. Deuxièmement, comme dans le modèle flexbox, il n’existe actuellement aucun sélecteur CSS pour cibler un élément en fonction de sa position dans la grille.

Cependant, comme nous utilisons quand même du javascript, vous pouvez parcourir tous les éléments de la grille et obtenir les propriétés de la grille CSS. S'ils correspondent aux coordonnées actuelles, vous avez votre élément cible. Malheureusement, cela ne fonctionnerait que si chaque élément était placé, utiliser grid-column-start: auto pour les éléments n’aide en rien. Même window.getComputedStyle() ne renverra que auto;

0
Paul

offsetTop est une méthode répandue pour déterminer la position y d'un élément. 

Si deux éléments frères adjacents ont la même position y, nous pouvons sans risque supposer qu'ils se trouvent visuellement sur la même rangée (tous les éléments ont la même hauteur). 

Ainsi, nous pouvons commencer à compter le nombre d'éléments d'une rangée en comparant leurs positions y un à un. Nous arrêtons de compter dès que nous sommes à court d'éléments ou lorsque nous rencontrons un frère ou une sœur adjacent avec une position y différente. 

function getCountOfItemsInRow() {
    let grid = document.getElementById('grid').children; //assumes #grid exists in dom
    let n = 0; // Zero items when grid is empty

    // If the grid has items, we assume the 0th element is in the first row, and begin counting at 1
    if (grid.length > 0) {
        n = 1; 

        // While the nth item has the same height as the previous item, count it as an item in the row. 
        while (grid[n] && grid[n].offsetTop === grid[n - 1].offsetTop) {
            n++;
        }
    }

    return n;
}
0
miir

Cet exemple suppose que le mouvement se termine aux limites. De même, si vous passez de la deuxième à la dernière ligne à la dernière, mais que le nombre de colonnes dans la dernière ligne est moins élevé, la dernière colonne passera à la dernière.

Cette solution assure le suivi des lignes/colonnes et utilise un objet de la grille pour savoir où se trouvent les éléments.

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: #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 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>

0
Emil