it-roy-ru.com

Как рассчитать количество элементов flexbox в строке?

Сетка реализована с использованием CSS flexbox. Пример:

 enter image description here

Количество строк в этом примере равно 4, потому что я установил ширину контейнера для демонстрационных целей. Но в действительности он может меняться в зависимости от ширины контейнера (например, если пользователь изменяет размеры окна). Попробуйте изменить размер окна вывода в этот пример , чтобы почувствовать.

Всегда есть один активный элемент, отмеченный черной рамкой.

Используя JavaScript, я позволяю пользователям переходить к предыдущему/следующему элементу, используя стрелку влево/вправо. В моей реализации я просто уменьшаю/увеличиваю индекс активного элемента на 1.

Теперь я бы хотел, чтобы пользователи также могли перемещаться вверх/вниз. Для этого мне просто нужно уменьшить/увеличить индекс активного элемента на <amount of items in a row>. Но как рассчитать это число, учитывая, что оно зависит от ширины контейнера? Есть ли лучший способ реализовать функциональность вверх/вниз?

.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

Вопрос немного сложнее, чем выяснить, сколько предметов подряд.

В конечном счете, мы хотим знать, есть ли элемент выше, ниже, слева и справа от активного элемента. И это необходимо учитывать в случаях, когда нижний ряд неполон. Например, в приведенном ниже случае активный элемент не имеет элемента выше, ниже или справа:

 enter image description here

Но, чтобы определить, есть ли элемент выше/ниже/слева/справа от активного элемента, нам нужно знать, сколько элементов в строке.

Найти количество предметов в строке

Чтобы получить количество элементов в строке нам нужно:

  • itemWidth - outerWidth одного элемента, включая border, padding и margin 
  • gridWidth - innerWidth таблицы, за исключением border, padding и margin

Чтобы вычислить эти два значения с помощью простого JavaScript, мы можем использовать:

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

Затем мы можем рассчитать количество элементов в строке, используя:

const numPerRow = Math.floor(gridWidth / itemWidth)

Примечание: это будет работать только для элементов одинакового размера, и только если margin определен в единицах px.

Гораздо более простой подход

Работа со всеми этими значениями ширины, отступов, полей и границ действительно сбивает с толку. Есть намного, намного, намного более простое решение.

Нам нужно только найти индекс элемента сетки, у которого свойство offsetTop больше, чем у первого элемента сетки 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);

Троичный в конце учитывает случаи, когда в сетке есть только один элемент и/или один ряд элементов.

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>

Но есть ли пункт выше или ниже?

Чтобы узнать, есть ли элемент выше или ниже активного элемента, нам нужно знать 3 параметра:

  • totalItemsInGrid
  • activeIndex
  • numPerRow

Например, в следующей структуре:

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

у нас есть totalItemsInGrid из 5, activeIndex имеет нулевой индекс 2 (это третий элемент в группе), и, скажем, numPerRow равен 3.

Теперь мы можем определить, есть ли элемент выше, ниже, слева или справа от активного элемента с помощью:

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

Если isTopRow равен true, мы не можем двигаться вверх, а если isBottomRow равен true, мы не можем двигаться вниз. Если isLeftColumn равен true, мы не можем двигаться влево, а если isRightColumn, если true, мы не можем двигаться вправо.

Note: isBottomRow не только проверяет, находится ли активный элемент в нижней строке, но также проверяет, есть ли элемент под ним. В нашем примере выше, активный элемент - это not в нижней строке, но под ним нет элемента.

Рабочий пример

Я сделал это в полном примере, который работает с изменением размера, и сделал элемент #grid изменяемым, чтобы его можно было проверить в приведенном ниже фрагменте. 

Я создал функцию navigateGrid, которая принимает три параметра:

  • gridSelector - селектор DOM для элемента сетки
  • activeClass - имя класса активного элемента
  • direction - один из up, down, left или right

Это можно использовать как 'navigateGrid("#grid", "active", "up") со структурой HTML из вашего вопроса.

Функция вычисляет количество строк с помощью метода offset, а затем проверяет, можно ли изменить элемент active на элемент up/down/left/right.

Другими словами, функция проверяет, можно ли перемещать активный элемент вверх/вниз и влево/вправо. Это означает:

  • не может идти влево от самого левого столбца
  • не может идти прямо из самого правого столбца
  • не может подняться с верхнего ряда
  • не может опуститься из нижнего ряда, или если ячейка ниже пуста 

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

(Для оптимального опыта лучше запускать интерактивные фрагменты на полной странице)

Расчет количества элементов в строке

Вам нужно получить ширину элемента с помощью его поля _ ​​(в конце концов, если они также установлены) тогда вам нужно получить внутреннюю ширину контейнера без заполнения. Имея эти 2 значения, вы делаете простое деление, чтобы получить количество элементов в строке.

Не забудьте рассмотреть случай, когда у вас есть только одна строка, поэтому вам нужно получить минимальное значение между общим количеством элементов и числом, которое вы получите от деления.

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

Вот версия jQuery той же логики с меньшим количеством кода:

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


А вот и демонстрация интерактивной сетки:

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>


Другая идея

Мы также можем рассмотреть другой способ навигации внутри сетки без количества элементов в строке. Идея состоит в том, чтобы положиться на функцию elementFromPoint(x,y) .

Логика такова: мы находимся внутри активного элемента, и у нас есть его позиция (x,y). Нажав клавишу, мы увеличим/уменьшим эти значения и используем вышеуказанную функцию, чтобы получить новый элемент, используя новый (x,y). Мы проверяем, получим ли мы допустимый элемент и является ли этот элемент элементом (содержит класс item). В этом случае мы удаляем активный из предыдущего и добавляем его в новый.

Вот пример, где я рассматриваю только inside навигацию. Когда мы достигнем левой/правой границы контейнера, мы не перейдем к предыдущей/следующей строке:

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>

Как вы можете заметить в этом методе, нам не нужна информация о контейнере, размере экрана, количестве элементов и т.д. Единственная необходимая информация - это размер одного элемента. Нам также нужен небольшой код, чтобы исправить положение активного элемента при изменении размера окна.


Бонус

Вот еще одна идея fancy, если вы хотите иметь активный элемент visually без необходимости добавлять класс или получать его с помощью JS. Идея состоит в том, чтобы использовать фон на контейнере, чтобы создать черный ящик за активным элементом.

Кстати, у этого метода есть 2 недостатка:

  1. Нелегко иметь дело с последней строкой, если она не полна элементов, так как черный ящик может быть позади ничего
  2. Мы должны учитывать пространство, оставшееся после последнего элемента каждой строки, чтобы избежать странного положения черного ящика.

Вот упрощенный код с контейнером фиксированной высоты/ширины:

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>

Как мы видим, код довольно прост, поэтому он может подойти для такой ситуации, когда почти все значения известны и фиксированы.

22
Temani Afif

Единственный способ перемещаться вверх и вниз, который вызывает меньше нежелательных осложнений, насколько я знаю, - это подсчитывать количество блоков в строке и изменять индексы. Единственная проблема заключается в том, что вам нужно рассчитать количество ящиков для загрузки окна и события изменения размера.

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

Теперь, если вы хотите очень простой способ получить количество блоков в строке , даже не заботясь о размере ни контейнера, ни блоков, забыть поля и отступы, вы можете проверить, сколько блоков выровнены по первому блоку сравнивая свойство offsetTop.

Свойство только для чтения HTMLElement.offsetTop возвращает расстояние текущего элемента относительно вершины узла offsetParent. [источник: developer.mozilla.orgl ]

Вы можете реализовать это как ниже: 

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

Полный рабочий пример:

(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

Чтобы поддержать перемещение вверх, вниз, влево и вправо, вам не нужно знать, сколько блоков в строке, нужно просто рассчитать, есть ли поле выше, ниже, влево или вправо от активного поля ,.

Перемещение влево и вправо, как вы уже заметили, очень просто - просто проверьте, есть ли в активном поле previousSiblingElement или nextSiblingElement. Для увеличения и уменьшения вы можете использовать текущий активный блок в качестве точки привязки и сравнить его с getBoundingClientRect()s другого блока, методом DOM , который возвращает геометрию элемента относительно области просмотра браузера.

При попытке подняться вверх, начните с якоря и начните обратный отсчет предметов до 0. При движении вниз начните с якоря и продолжайте отсчет до конца количества предметов. Это связано с тем, что при движении вверх мы заботимся только о ящиках перед активным ящиком, а при спуске мы заботимся только о ящиках после него. Все, что нам нужно искать, - это поле, которое имеет одинаковую левую позицию с верхней или нижней верхней позицией.

Ниже приведен пример, который прослушивает событие нажатия клавиши на window и перемещает активное состояние в соответствии с тем, какая клавиша со стрелкой была нажата. Это определенно можно сделать более СУХОЙ, но я разделил четыре случая, чтобы вы могли видеть точную логику в каждом. Вы можете удерживать клавиши со стрелками вниз, чтобы коробка двигалась непрерывно, и вы можете видеть, что она очень эффективна. И я обновил ваш JSBin своим решением здесь: 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

Хотя вы можете рассчитать, какой элемент вы ищете, я предлагаю вам поискать элемент ниже. Преимущество этого состоит в том, что это будет работать, даже если ваши элементы не имеют одинаковую ширину.

Итак, давайте подумаем об атрибутах элемента ниже. По сути, это первый элемент с более высокой offsetTop и той же offsetLeft. Вы можете сделать что-то вроде этого, чтобы найти элемент 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

Этот пример предполагает, что движение заканчивается в границах. Кроме того, при переходе от второй к последней строке до последней строки, но в последнем ряду столбцов меньше, вместо этого он переместится в последний столбец последней строки.

Это решение отслеживает строки/столбцы и использует объект сетки, чтобы отслеживать, где находятся элементы. Позиции будут обновлены в объекте сетки при изменении размера страницы.

(вы можете увидеть обновление упаковки в действии в полноэкранном режиме)

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

Если вы используете Jquery и уверены, что ваши объекты сетки выровнены по вертикали, это может помочь. 

Я не проверял это, но он должен работать (считая столбцы)

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

Вы можете использовать Array.prototype.filter (), чтобы сделать это довольно аккуратно . Чтобы получить количество элементов в строке, используйте эту функцию . Передайте селектор CSS, который вы хотите использовать (в данном случае .item) . Как только у вас будет размер строки, навигация по стрелкам будет легкой. 

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

Примеры

Демонстрация CodePen: https://codepen.io/gtlitc/pen/EExXQE

Интерактивная демонстрация, которая отображает размер строки и суммы перемещения . http://www.smallblue.net/demo/49043684/

Объяснение

Во-первых, функция устанавливает переменную firstTop как offsetTop самого первого узла.

Затем функция строит массив rowArray узлов в первой строке (если возможна навигация вверх и вниз, первая строка всегда будет строкой полной длины).

Это делается путем вызова (заимствования) функции фильтра из прототипа массива. Мы не можем просто вызвать функцию фильтра в списке узлов, который возвращается QSA (все селекторы запросов), потому что браузеры возвращают списки узлов вместо массивов, а списки узлов не являются правильными массивами.

Тогда оператор if просто фильтрует все узлы и возвращает только те, которые имеют ту же переменную offsetTop, что и первый узел. все узлы в первом ряду.

Теперь у нас есть массив, из которого мы можем определить длину строки.

Я пропустил реализацию обхода DOM, поскольку это просто с использованием чистого Javascript или Jquery и т.д. И не было частью вопроса о OP. Я хотел бы только отметить, что важно проверить, существует ли элемент, в который вы собираетесь переместить, перед тем, как переехать туда.

Эта функция будет работать с любой техникой компоновки . Flexbox, float, CSS grid, что бы ни было в будущем.

Рекомендации

Почему document.querySelectorAll возвращает StaticNodeList, а не реальный массив?

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

0
George Litchfield

Я знаю, что это не совсем то, о чем просит OP, но я хотел показать возможную альтернативу (зависит от варианта использования).

Вместо использования CSS flexbox, есть также более поздняя CSS-сетка, которая фактически содержит столбцы и строки. Таким образом, путем преобразования структуры в сетку и использования некоторого JS для прослушивания нажатия клавиш, активный элемент может быть перемещен (см. Неполный рабочий пример ниже). 

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>

Однако, как указано выше, это решение имеет недостатки. На этот раз активный элемент сам по себе является элементом сетки и перемещается вдоль сетки, а остальные элементы обтекают его. Во-вторых, как и в модели flexbox, в настоящее время нет CSS-селекторов для целевого элемента на основе его позиции в сетке.

Однако, поскольку мы все равно используем javascript, вы можете перебрать все элементы сетки и получить свойства CSS Grid. Если они соответствуют текущим координатам, у вас есть целевой элемент. К сожалению, это будет работать, только если каждый элемент размещен, использование grid-column-start: auto для элементов не помогает. Даже window.getComputedStyle() вернет только auto;

0
Paul

offsetTop - это популярный метод определения y-позиции элемента. 

Если два соседних элемента родного брата имеют одинаковую y-позицию, мы можем смело предположить, что они визуально находятся в одном ряду (поскольку все элементы имеют одинаковую высоту). 

Таким образом, мы можем начать подсчет количества элементов в строке, сравнивая их y-позиции один за другим. Мы прекращаем считать, как только у нас заканчиваются элементы или мы сталкиваемся с соседним братом с другой y-позицией. 

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

Этот пример предполагает, что движение заканчивается в границах. Кроме того, при переходе от второй к последней строке до последней строки, но в последнем ряду столбцов меньше, вместо этого он переместится в последний столбец последней строки.

Это решение отслеживает строки/столбцы и использует объект сетки, чтобы отслеживать, где находятся элементы.

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