【问题标题】:How to calculate the amount of flexbox items in a row?如何计算一行中flexbox项目的数量?
【发布时间】:2018-08-09 03:59:56
【问题描述】:

网格是使用 CSS flexbox 实现的。 Example:

此示例中的行数为 4,因为我为演示目的固定了容器宽度。但是,实际上,它可以根据容器的宽度而改变(例如,如果用户调整窗口大小)。尝试调整this example 中的输出窗口大小,感受一下。

总是有一个活动项目,用黑色边框标记。

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

【问题讨论】:

  • 你的意思是连续的“数量”项目吗?
  • 获取容器宽度。获取子元素宽度。并一一分割?
  • @JacobGoh 这种简单方法的问题是您需要考虑项目/容器的填充/边距。不知道有没有更好的办法。
  • outerWidth 将考虑项目/容器的填充/边距。它是 jquery,但也应该有一个 vanilla js 方式。
  • 所有网格元素的大小是否一致 - 高度和宽度?

标签: javascript html css flexbox


【解决方案1】:

要支持上下左右移动,你不需要知道一排有多少个盒子,你只需要计算上下左右是否有一个盒子活动框。

如您所见,左右移动很简单 - 只需检查活动框是否有 previousSiblingElementnextSiblingElement。对于 up 和 down,您可以使用当前活动框作为锚点,并将其与其他框的 getBoundingClientRect()s 进行比较,DOM method 返回元素相对于浏览器视口的几何形状。

尝试向上移动时,从锚点开始,将项目数倒数至 0。向下移动时,从锚点开始计数,直到项目数结束。这是因为向上移动时,我们只关心活动盒子之前的盒子,而向下移动时,我们只关心它之后的盒子。我们只需要寻找一个左侧位置相同但顶部位置更高或更低的盒子。

下面是一个示例,它侦听window 上的 keydown 事件,并将根据按下的箭头键移动活动状态。它肯定可以做得更干,但我已经把这四种情况分开了,这样你就可以看到每种情况的确切逻辑。您可以按住箭头键,使框连续移动,您可以看到它的性能非常好。我在这里用我的解决方案更新了你的 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】:

    这个问题比查找一行中有多少项要复杂一些。

    最终,我们想知道活动元素的上方、下方、左侧和右侧是否有一个元素。这需要考虑到最后一行不完整的情况。例如,在下面的情况下,活动元素没有上、下或右项:

    但是,为了确定活动项目的上方/下方/左侧/右侧是否有项目,我们需要知道一行中有多少项目。

    查找每行的项目数

    要获取我们需要的每行项目数:

    • itemWidth - 单个元素的outerWidth,包括borderpaddingmargin
    • gridWidth - 网格的innerWidth,不包括borderpaddingmargin

    要使用纯 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)
    

    注意:这仅适用于统一大小的项目,并且仅当marginpx 为单位定义时。

    一种非常、非常、非常简单的方法

    处理所有这些宽度、内边距、边距和边框确实令人困惑。有一个非常非常简单的解决方案。

    我们只需要找到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>
    

    我们有 5 中的 totalItemsInGridactiveIndex 有一个从零开始的索引 2(它是组中的第三个元素),假设 numPerRow 是 3。

    我们现在可以确定活动项目的上方、下方、左侧或右侧是否有项目:

    • isTopRow = activeIndex &lt;= numPerRow - 1
    • isBottomRow = activeIndex &gt;= totalItemsInGid - numPerRow
    • isLeftColumn = activeIndex % numPerRow === 0
    • isRightColumn = activeIndex % numPerRow === numPerRow - 1 || activeIndex === gridNum - 1

    如果isTopRowtrue 我们不能向上移动,如果isBottomRowtrue 我们不能向下移动。如果isLeftColumntrue 我们不能向左移动,如果isRightColumn 如果true 我们不能向右移动。

    注意isBottomRow 不仅会检查活动元素是否在底行,还会检查其下方是否有元素。在我们上面的示例中,活动元素是 不是 在底行,但在其下方没有项目。

    一个工作示例

    我已经把它变成了一个可以调整大小的完整示例 - 并使 #grid 元素可调整大小,以便可以在下面的 sn-p 中对其进行测试。

    我创建了一个函数,navigateGrid,它接受三个参数:

    • gridSelector - 网格元素的 DOM 选择器
    • activeClass - 活动元素的类名
    • direction - updownleftright 之一

    这可以用作'navigateGrid("#grid", "active", "up") 与您问题中的 HTML 结构。

    该函数使用offset 方法计算行数,然后检查active 元素是否可以更改为上/下/左/右元素。

    换句话说,该函数检查活动元素是否可以向上/向下和向左/向右移动。这意味着:

    • 不能从最左边的列向左移动
    • 不能从最右边的列开始
    • 不能从顶行上升
    • 不能从最下面一行往下走,或者如果下面的单元格是空的

    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>

    【讨论】:

    • 使用offsetTop 很聪明,感谢这个好主意!
    【解决方案3】:

    您可以使用 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(如果可以上下导航,则第一行将始终是全长行)。

    这是通过调用(借用)Array Prototype 中的过滤器函数来完成的。我们不能简单地在 QSA(查询选择器全部)返回的节点列表上调用过滤器函数,因为浏览器返回节点列表而不是数组,并且节点列表不是正确的数组。

    if 语句然后简单地过滤所有节点并仅返回与第一个节点具有相同offsetTop 的节点。即第一行中的所有节点。

    我们现在有一个数组,我们可以从中确定一行的长度。

    我省略了 DOM 遍历的实现,因为这很简单,使用纯 Javascript 或 Jquery 等,并且不是 OPs 问题的一部分。我只想指出,在移动到那里之前测试您打算移动到的元素是否存在是很重要的。

    此功能适用于任何布局技术。 Flexbox、float、CSS 网格,无论未来如何。

    参考文献

    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

    【讨论】:

      【解决方案4】:

      如果您使用的是 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
      }
      

      【讨论】:

      • 如果你不使用jquery,你可以通过使用“document.getElementById('grid').children”或“document.getElementByClassName('grid-object')”得到相同的结果
      【解决方案5】:

      据我所知,上下移动以减少不必要的复杂性的唯一方法是计算每行的框数并更改索引。唯一的问题是您需要计算窗口加载和调整大小事件的 boxcount。

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

      现在,如果您想要一种非常简单的方法来获取一行中的盒子数量甚至不关心容器和盒子的大小忘记边距和填充 strong>,您可以检查有多少个框与第一个框对齐比较 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>

      【讨论】:

      • 如果你不反感 jQuery,我很喜欢这个:$('.item').filter(function () { return this.offsetTop == $( .item':first-child')[0].offsetTop; }).length
      【解决方案6】:

      此示例假定运动在边界处结束。另外,如果从倒数第二行向下移动到最后一行,但最后一行的列数较少,则改为移动到最后一行的最后一列。

      此解决方案跟踪行/列,并使用网格对象来跟踪元素的位置。调整页面大小时,网格对象中的位置将更新。

      (您可以在全屏模式下看到包装更新)

      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>

      【讨论】:

        【解决方案7】:

        此示例假定运动在边界处结束。此外,如果从倒数第二行向下移动到最后一行,但最后一行的列数较少,则会改为移动到最后一行的最后一列。

        此解决方案跟踪行/列并使用网格对象来跟踪元素的位置。

        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>

        【讨论】:

          【解决方案8】:

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

          【讨论】:

            【解决方案9】:

            (为了获得最佳体验,最好在整页上运行交互式 sn-ps)

            计算每行的元素数

            你需要得到一个元素的宽度 它的边距 (如果它们也设置了最后是边框) 然后你需要得到容器的内部宽度 没有填充。有了这 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 类)。在这种情况下,我们从前一个中删除 active 并将其添加到新的中。

            这是一个我只考虑 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>

            你可能注意到,在这个方法中,我们不需要任何关于容器、屏幕大小、元素数量等的信息。唯一需要的信息是单个项目的尺寸。我们还需要一个小代码来纠正窗口调整大小时活动元素的位置。


            奖金

            如果您想拥有一个 视觉上 活动元素而不需要添加类或使用 JS 获取它,这是另一个花哨的想法。这个想法是使用容器上的背景在活动元素后面创建一个黑框。

            顺便说一下,这种方法有两个缺点:

            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>

            正如我们所见,代码非常简单,因此适用于几乎所有值都已知且固定的情况。

            【讨论】:

            • 应该注意,窗口大小调整并不是容器大小改变的唯一可能原因。理想情况下会使用ResizeObserver,但目前是barely supported at all
            • @H.B.对,我第一次来这里:)但它特别适用于动态/交互式页面。这个功能以后真的很有用,我已经在想怎么用了;)
            • 它没有更早更广泛地实施,这真的很有用,也很遗憾。目前,一种常见的解决方法是使用getBoundingClientRect() 运行一个检查容器大小的间隔,这显然不理想,因为它需要重复布局计算,这在没有任何变化的情况下应该是不必要的。
            • 嗨,我是初学者,只是想知道,你为什么在这里使用 Math.min: var nb = Math.min(parseInt((w_c - p_c) / w),n_t);实际上,我明白为什么,但我想我不明白为什么没有 Math.min 就不能做到这一点,而只是通过 parseInt((w_c - p_c) / w)
            • @Alinacdn 这是您有一行的情况,例如,除法可能会给您 10,但项目只有 8 + 一些空白空间。因此,您需要使用 min 来避免大于项目数...尝试使用少数项目数(2 或 3)的示例,然后删除 min 以查看您获得的值
            【解决方案10】:

            虽然您可以计算出您要查找的元素,但我建议您搜索下面的元素。这样做的好处是,如果您的元素没有相同的宽度,它甚至可以工作。

            所以让我们考虑一下下面元素的属性。本质上它是第一个具有更高offsetTop 和相同offsetLeft 的元素。您可以执行以下操作来找到顶部的元素:

            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)
            

            【讨论】:

              【解决方案11】:

              我知道这并不是 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-column-start: auto 无济于事。即使window.getComputedStyle()也只会返回auto

              【讨论】:

              • 这个解决方案的另一个缺点是使用固定宽度/高度,这是这个问题的主要状态......好像网格是固定的,使用网格、flexbox、甚至浮动元素。 .. 但如果我们想在相邻元素之间切换或在空网格内移动单个元素,这是一种有趣的考虑方式。
              猜你喜欢
              • 2023-03-12
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2012-12-20
              • 1970-01-01
              • 1970-01-01
              • 2020-12-09
              • 1970-01-01
              相关资源
              最近更新 更多