代码之家  ›  专栏  ›  技术社区  ›  Denver Dang

根据元素的宽度/大小对多个FlexBox行中的元素排序

  •  2
  • Denver Dang  · 技术社区  · 7 年前

    所以假设我有一组这样的列表项(一些类别):

    ul.categoryList {
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      width: 180px;
    }
    
    ul.categoryList > li {
      list-style: none;
    }
    <ul class="categoryList">
      <li>Literature</li>
      <li>Science Fiction and Fantasy</li>
      <li>Harry Potter</li>
      <li>Movies and Films</li>
      <li>Books</li>
    </ul>

    还有这个 <ul> 在A内 <div> 用一个 max-width 如果调整窗口大小或在不同的分辨率/设备(移动设备、平板电脑等)上,这可能会发生变化。

    如您所见,某些列表项比其他项长。假设这个容器 <UL & GT; 只能包含列表项 Science Fiction and Fantasy 还有一点,所以下一个项目将转到下一行,因为它不适合同一个项目。

    正如你所看到的,问题是 Literature Books 可以在同一行上同时出现,但由于它们不是连续的,因此它们将以分隔的行结束,这同样适用于其他项。

    因此,我得到了5行(实际上每个项目一行),而不是将一些最短的项目放在一起以减少行的消耗,这是一个空间消耗。

    有什么办法可以解决这个问题吗?可以只使用CSS来完成,还是需要JavaScript?

    1 回复  |  直到 7 年前
        1
  •  1
  •   Danziger    7 年前

    按字符数对元素排序

    您需要使用javascript根据元素的长度/大小对其进行排序。

    这是一个使用 Array.prototype.sort() 根据每个字符的字符数对它们进行排序( Node.innerText ):

    // Sort the elements according to their number of characters:
    
    const categoryList = document.getElementById('categoryList');
    
    Array.from(categoryList.children).sort((a, b) => {
      const charactersA = a.innerText.length;
      const charactersB = b.innerText.length;
      
      if (charactersA < charactersB) {
        return -1;
      } else if (charactersA === charactersB) {
        return 0;
      } else {
        return 1;
      }
    }).forEach((element) => {
      // When appending an element that is already a child, it will not
      // be duplicated, but removed from the old position first and then
      // added to the new one, which is exactly what we want:
      
      categoryList.appendChild(element);
    });
    #categoryList {
      font-family: monospace;
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      align-content: flex-start;
      list-style: none;
      padding: 0;
      margin: 0;
      width: 220px;
      border-right: 2px solid #000;
    }
    
    #categoryList > li {
      background: #000;
      color: #FFF;
      padding: 4px 8px;
      margin: 0 4px 4px 0;
      border-radius: 2px;
    }
    <ul id="categoryList">
      <li>Literature</li>
      <li>Science Fiction and Fantasy</li>
      <li>Harry Potter</li>
      <li>Movies and Films</li>
      <li>Books</li>
    </ul>

    按实际宽度对元素排序

    innerText 对于单间距字体可能很好,但对于其他字体,您可以使用 HTMLElement.offsetWidth 而是考虑元素的实际宽度:

    /**
    * Get the actual width of an element, taking into account margins 
    * as well:
    */
    function getElementWidth(element) {
      const style = window.getComputedStyle(element);
      
      // Assuming margins are in px:
      return element.offsetWidth + parseInt(style.marginLeft) + parseInt(style.marginRight);
    }
    
    
    // Sort the elements according to their actual width:
    
    const categoryList = document.getElementById('categoryList');
    
    Array.from(categoryList.children).sort((a, b) => {
      const aWidth = getElementWidth(a);
      const bWidth = getElementWidth(b);
      
      if (aWidth < bWidth) {
        return -1;
      } else if (aWidth === bWidth) {
        return 0;
      } else {
        return 1;
      }
    }).forEach((element) => {
      // When appending an element that is already a child, it will not
      // be duplicated, but removed from the old position first and then
      // added to the new one, which is exactly what we want:
      
      categoryList.appendChild(element);
    });
    #类别列表{
    字体系列:monospace;
    显示器:柔性;
    弯曲方向:行;
    柔性包装:包装;
    内容对齐:弹性开始;
    列表样式:无;
    填充:0;
    边距:0;
    宽度:220px;
    右边框:2倍纯色000;
    }
    
    #类别列表{
    背景:000;
    颜色:fff;
    填料:4px 8px;
    裕度:0 4px 4px 0;
    边界半径:2px;
    }
    <ul id=“categorylist”>
    <li>文献</li>
    <li>科幻和幻想</li>
    <li>哈利波特</li>
    <li>电影和电影</li>
    <li>书籍</li>
    </ul>

    对元素进行排序以最小化空空间

    您还可以实现自定义排序算法,以不同的方式对它们进行排序。例如,您可能希望最小化每行上的空白:

    /**
    * Get the actual width of an element, taking into account margins 
    * as well:
    */
    function getElementWidth(element) {
      const style = window.getComputedStyle(element);
      
      // Assuming margins are in px:
      return element.offsetWidth + parseInt(style.marginLeft) + parseInt(style.marginRight);
    }
    
    /**
    * Find the index of the widest element that fits in the available
    * space:
    */
    function getBestFit(elements, availableSpace) {
      let minAvailableSpace = availableSpace;
      let bestFitIndex = -1;
      
      elements.forEach((element, i) => {
        if (element.used) {
          return;
        }
        
        const elementAvailableSpace = availableSpace - element.width;
        
        if (elementAvailableSpace >= 0 && elementAvailableSpace < minAvailableSpace) {
          minAvailableSpace = elementAvailableSpace;
          bestFitIndex = i;
        }
      });
      
      return bestFitIndex;
    }
    
    /**
    * Get the first element that hasn't been used yet.
    */
    function getFirstNotUsed(elements) {
      for (let element of elements) {
        if (!element.used) {
          return element;
        }
      }
    }
    
    
    // Sort the elements according to their actual width:
    
    const categoryList = document.getElementById('categoryList');
    const totalSpace = categoryList.clientWidth;
    const items = Array.from(categoryList.children).map((element) => {
      return {
        element,
        used: false,
        width: getElementWidth(element),
      };
    });
    const totalItems = items.length;
    
    // We want to keep the first element in the first position:
    const firstItem = items[0];
    const sortedElements = [firstItem.element];
    
    firstItem.used = true;
    
    // We calculate the remaining space in the first row:
    let availableSpace = totalSpace - firstItem.width;
    
    // We sort the other elements:
    for (let i = 1; i < totalItems; ++i) {
      const bestFitIndex = getBestFit(items, availableSpace);
      
      let item;
      
      if (bestFitIndex === -1) {
        // If there's no best fit, we just take the first element
        // that hasn't been used yet to keep their order as close
        // as posible to the initial one:
        item = getFirstNotUsed(items);
        availableSpace = totalSpace - item.width;
      } else {
        item = items[bestFitIndex];
        availableSpace -= item.width;
      }
      
      sortedElements.push(item.element);  
      item.used = true;
    }
    
    sortedElements.forEach((element) => {
      // When appending an element that is already a child, it will not
      // be duplicated, but removed from the old position first and then
      // added to the new one, which is exactly what we want:
      
      categoryList.appendChild(element);
    });
    #类别列表{
    字体系列:monospace;
    显示器:柔性;
    弯曲方向:行;
    柔性包装:包装;
    内容对齐:弹性开始;
    列表样式:无;
    填充:0;
    边距:0;
    宽度:220px;
    右边框:2倍纯色000;
    }
    
    #类别列表{
    背景:000;
    颜色:fff;
    填料:4px 8px;
    裕度:0 4px 4px 0;
    边界半径:2px;
    }
    <ul id=“categorylist”>
    <li>文献</li>
    <li>科幻和幻想</li>
    <li>哈利波特</li>
    <li>电影和电影</li>
    <li>书籍</li>
    </ul>

    ¨让它看起来更好

    最后,你可以申请 flex: 1 0 auto 对列表中的每个孩子进行排序以删除他们之间的任何不规则空格后:

    /**
    * Get the actual width of an element, taking into account margins 
    * as well:
    */
    function getElementWidth(element) {
      const style = window.getComputedStyle(element);
      
      // Assuming margins are in px:
      return element.offsetWidth + parseInt(style.marginLeft) + parseInt(style.marginRight);
    }
    
    /**
    * Find the index of the widest element that fits in the available
    * space:
    */
    function getBestFit(elements, availableSpace) {
      let minAvailableSpace = availableSpace;
      let bestFitIndex = -1;
      
      elements.forEach((element, i) => {
        if (element.used) {
          return;
        }
        
        const elementAvailableSpace = availableSpace - element.width;
        
        if (elementAvailableSpace >= 0 && elementAvailableSpace < minAvailableSpace) {
          minAvailableSpace = elementAvailableSpace;
          bestFitIndex = i;
        }
      });
      
      return bestFitIndex;
    }
    
    /**
    * Get the first element that hasn't been used yet.
    */
    function getFirstNotUsed(elements) {
      for (let element of elements) {
        if (!element.used) {
          return element;
        }
      }
    }
    
    
    // Sort the elements according to their actual width:
    
    const categoryList = document.getElementById('categoryList');
    const totalSpace = categoryList.clientWidth;
    const items = Array.from(categoryList.children).map((element) => {
      return {
        element,
        used: false,
        width: getElementWidth(element),
      };
    });
    const totalItems = items.length;
    
    // We want to keep the first element in the first position:
    const firstItem = items[0];
    const sortedElements = [firstItem.element];
    
    firstItem.used = true;
    
    // We calculate the remaining space in the first row:
    let availableSpace = totalSpace - firstItem.width;
    
    // We sort the other elements:
    for (let i = 1; i < totalItems; ++i) {
      const bestFitIndex = getBestFit(items, availableSpace);
      
      let item;
      
      if (bestFitIndex === -1) {
        // If there's no best fit, we just take the first element
        // that hasn't been used yet to keep their order as close
        // as posible to the initial one:
        item = getFirstNotUsed(items);
        availableSpace = totalSpace - item.width;
      } else {
        item = items[bestFitIndex];
        availableSpace -= item.width;
      }
      
      sortedElements.push(item.element);  
      item.used = true;
    }
    
    sortedElements.forEach((element) => {
      // When appending an element that is already a child, it will not
      // be duplicated, but removed from the old position first and then
      // added to the new one, which is exactly what we want:
      
      categoryList.appendChild(element);
    });
    
    // If you want to add a class to make the elements inside the list
    // expand, you have to do it after sorting them. Otherwise, they would
    // already take all available horizontal space and the sorting algorithm
    // won't do anything:
    categoryList.classList.add('expand');
    #categoryList {
      font-family: monospace;
      display: flex;
      flex-direction: row;
      flex-wrap: wrap;
      align-content: flex-start;
      list-style: none;
      padding: 0;
      margin: 0;
      width: 220px;
      border-right: 2px solid #000;
    }
    
    #categoryList > li {
      background: #000;
      color: #FFF;
      padding: 4px 8px;
      margin: 0 4px 4px 0;
      border-radius: 2px;
    }
    
    #categoryList.expand > li {
      flex: 1 1 auto;
    }
    <ul id=“categorylist”>
    <li>文献</li>
    <li>科幻和幻想</li>
    <li>哈利波特</li>
    <li>电影和电影</li>
    <li>书籍</li>
    </ul>