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

Den*_*ang 4 javascript css sorting listitem flexbox

假设我有一组这样的列表项(某种类别):

ul.categoryList {
  display: flex;
  flex-direction: row;
  flex-wrap: wrap;
  width: 180px;
}

ul.categoryList > li {
  list-style: none;
}
Run Code Online (Sandbox Code Playgroud)
<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>
Run Code Online (Sandbox Code Playgroud)

<ul>是在 a 内<div>max-width如果调整窗口大小或在不同的分辨率/设备(手机、平板电脑等)上, a 可以更改。

正如您所看到的,某些列表项比其他项长。假设此容器<ul>只能包含列表项Science Fiction and Fantasy和更多内容,因此下一项将转到下一行,因为它不适合同一行。

正如您可能看到的那样,问题在于LiteratureBooks可能位于同一行,但由于它们不连续,因此它们最终会出现在单独的行上,这同样适用于其他项目。

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

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

Dan*_*ger 5

按字符数对元素进行排序

\n\n

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

\n\n

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

\n\n

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

\n\n

按实际宽度对元素进行排序

\n\n

innerText对于等宽字体可能效果很好,但对于其他字体,您可以使用HTMLElement.offsetWidth它来考虑元素的实际宽度:

\n\n

\r\n
\r\n
/**\r\n* Get the actual width of an element, taking into account margins \r\n* as well:\r\n*/\r\nfunction getElementWidth(element) {\r\n  const style = window.getComputedStyle(element);\r\n  \r\n  // Assuming margins are in px:\r\n  return element.offsetWidth + parseInt(style.marginLeft) + parseInt(style.marginRight);\r\n}\r\n\r\n\r\n// Sort the elements according to their actual width:\r\n\r\nconst categoryList = document.getElementById(\'categoryList\');\r\n\r\nArray.from(categoryList.children).sort((a, b) => {\r\n  const aWidth = getElementWidth(a);\r\n  const bWidth = getElementWidth(b);\r\n  \r\n  if (aWidth < bWidth) {\r\n    return -1;\r\n  } else if (aWidth === bWidth) {\r\n    return 0;\r\n  } else {\r\n    return 1;\r\n  }\r\n}).forEach((element) => {\r\n  // When appending an element that is already a child, it will not\r\n  // be duplicated, but removed from the old position first and then\r\n  // added to the new one, which is exactly what we want:\r\n  \r\n  categoryList.appendChild(element);\r\n});
Run Code Online (Sandbox Code Playgroud)\r\n
#categoryList {\r\n  font-family: monospace;\r\n  display: flex;\r\n  flex-direction: row;\r\n  flex-wrap: wrap;\r\n  align-content: flex-start;\r\n  list-style: none;\r\n  padding: 0;\r\n  margin: 0;\r\n  width: 220px;\r\n  border-right: 2px solid #000;\r\n}\r\n\r\n#categoryList > li {\r\n  background: #000;\r\n  color: #FFF;\r\n  padding: 4px 8px;\r\n  margin: 0 4px 4px 0;\r\n  border-radius: 2px;\r\n}
Run Code Online (Sandbox Code Playgroud)\r\n
<ul id="categoryList">\r\n  <li>Literature</li>\r\n  <li>Science Fiction and Fantasy</li>\r\n  <li>Harry Potter</li>\r\n  <li>Movies and Films</li>\r\n  <li>Books</li>\r\n</ul>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n\n

对元素进行排序,最大限度地减少空白

\n\n

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

\n\n

\r\n
\r\n
/**\r\n* Get the actual width of an element, taking into account margins \r\n* as well:\r\n*/\r\nfunction getElementWidth(element) {\r\n  const style = window.getComputedStyle(element);\r\n  \r\n  // Assuming margins are in px:\r\n  return element.offsetWidth + parseInt(style.marginLeft) + parseInt(style.marginRight);\r\n}\r\n\r\n/**\r\n* Find the index of the widest element that fits in the available\r\n* space:\r\n*/\r\nfunction getBestFit(elements, availableSpace) {\r\n  let minAvailableSpace = availableSpace;\r\n  let bestFitIndex = -1;\r\n  \r\n  elements.forEach((element, i) => {\r\n    if (element.used) {\r\n      return;\r\n    }\r\n    \r\n    const elementAvailableSpace = availableSpace - element.width;\r\n    \r\n    if (elementAvailableSpace >= 0 && elementAvailableSpace < minAvailableSpace) {\r\n      minAvailableSpace = elementAvailableSpace;\r\n      bestFitIndex = i;\r\n    }\r\n  });\r\n  \r\n  return bestFitIndex;\r\n}\r\n\r\n/**\r\n* Get the first element that hasn\'t been used yet.\r\n*/\r\nfunction getFirstNotUsed(elements) {\r\n  for (let element of elements) {\r\n    if (!element.used) {\r\n      return element;\r\n    }\r\n  }\r\n}\r\n\r\n\r\n// Sort the elements according to their actual width:\r\n\r\nconst categoryList = document.getElementById(\'categoryList\');\r\nconst totalSpace = categoryList.clientWidth;\r\nconst items = Array.from(categoryList.children).map((element) => {\r\n  return {\r\n    element,\r\n    used: false,\r\n    width: getElementWidth(element),\r\n  };\r\n});\r\nconst totalItems = items.length;\r\n\r\n// We want to keep the first element in the first position:\r\nconst firstItem = items[0];\r\nconst sortedElements = [firstItem.element];\r\n\r\nfirstItem.used = true;\r\n\r\n// We calculate the remaining space in the first row:\r\nlet availableSpace = totalSpace - firstItem.width;\r\n\r\n// We sort the other elements:\r\nfor (let i = 1; i < totalItems; ++i) {\r\n  const bestFitIndex = getBestFit(items, availableSpace);\r\n  \r\n  let item;\r\n  \r\n  if (bestFitIndex === -1) {\r\n    // If there\'s no best fit, we just take the first element\r\n    // that hasn\'t been used yet to keep their order as close\r\n    // as posible to the initial one:\r\n    item = getFirstNotUsed(items);\r\n    availableSpace = totalSpace - item.width;\r\n  } else {\r\n    item = items[bestFitIndex];\r\n    availableSpace -= item.width;\r\n  }\r\n  \r\n  sortedElements.push(item.element);  \r\n  item.used = true;\r\n}\r\n\r\nsortedElements.forEach((element) => {\r\n  // When appending an element that is already a child, it will not\r\n  // be duplicated, but removed from the old position first and then\r\n  // added to the new one, which is exactly what we want:\r\n  \r\n  categoryList.appendChild(element);\r\n});
Run Code Online (Sandbox Code Playgroud)\r\n
#categoryList {\r\n  font-family: monospace;\r\n  display: flex;\r\n  flex-direction: row;\r\n  flex-wrap: wrap;\r\n  align-content: flex-start;\r\n  list-style: none;\r\n  padding: 0;\r\n  margin: 0;\r\n  width: 220px;\r\n  border-right: 2px solid #000;\r\n}\r\n\r\n#categoryList > li {\r\n  background: #000;\r\n  color: #FFF;\r\n  padding: 4px 8px;\r\n  margin: 0 4px 4px 0;\r\n  border-radius: 2px;\r\n}
Run Code Online (Sandbox Code Playgroud)\r\n
<ul id="categoryList">\r\n  <li>Literature</li>\r\n  <li>Science Fiction and Fantasy</li>\r\n  <li>Harry Potter</li>\r\n  <li>Movies and Films</li>\r\n  <li>Books</li>\r\n</ul>
Run Code Online (Sandbox Code Playgroud)\r\n
\r\n
\r\n

\n\n

\xe2\x9c\xa8 让它看起来更好

\n\n

最后,您可以flex: 1 0 auto在对列表中的每个子项进行排序后应用它们,以删除它们之间的任何不规则的空白:

\n\n

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

\n