如何在 CSS 多栏布局中插入分栏符?

How can I insert column break in a CSS multi-column layout?

我正在尝试实现一个大型菜单。

菜单项的数量是可变的。默认情况下,它们必须以 4 列 平衡呈现(每列上的项目数应与其他列几乎相同)。 大型菜单的高度也是可变的,具体取决于其内容。

我已经用 CSS 多列布局 实现了它。

代码是:

.menu {
  -webkit-column-count: 4;
     -moz-column-count: 4;
          column-count: 4;
 -webkit-column-gap: 32px;
    -moz-column-gap: 32px;
         column-gap: 32px;
}

我的问题是有一个特殊的菜单项类型,它应该充当 分栏符。此菜单项类型是可选的,但如果存在,它应该强制浏览器开始一个新列来显示内容(最多可以有 3 个分栏符)。

我添加了以下 css 代码:

.menu-item--column-break {
    display: block;
    -webkit-column-break-before: column;
              -moz-break-before: column;
                   break-before: column;
}

但是这个 CSS 只适用于 Chrome:

Firefox 和 Safari 不支持 "column-break" 元素的 CSS 规则,并像普通菜单项一样显示它:

菜单是在 JavaScript 中从 JSON 对象生成的,HTML 可以更改,但我更喜欢 CSS/JS-only 解决方案。

你知道我如何在所有浏览器中实现它吗?

完整代码如下:

https://codepen.io/andreivictor/pen/ywLJKx

let items = [
  {title: 'Category 1', type: 'menu-item'},
  {title: 'Category 2', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 3', type: 'menu-item'},
  {title: 'Category 4', type: 'menu-item'},
  {title: 'Category 5', type: 'menu-item'},
  {title: 'Category 6', type: 'menu-item'},
  {title: 'Category 7', type: 'menu-item'},
  {title: 'Category 8', type: 'menu-item'},
  {title: 'Category 9', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 10', type: 'menu-item'},
  {title: 'Category 11', type: 'menu-item'},
  {title: 'Category 12', type: 'menu-item'},
  {title: 'Category 13', type: 'menu-item'},
  {title: 'Category 14', type: 'menu-item'},
  {title: 'Category 15', type: 'menu-item'},
  {title: 'Category 16', type: 'menu-item'},
  {title: 'Category 17', type: 'menu-item'},
  {title: 'Category 18', type: 'menu-item'},
  {title: 'Category 19', type: 'menu-item'},
  {title: 'Category 20', type: 'menu-item'},
  {title: 'Category 21', type: 'menu-item'},
];

const $menu = document.querySelector('.menu');

console.log( $menu );

items.forEach((item) => {
  let nodeItem = document.createElement("div");
  nodeItem.classList.add('menu-item');
  let nodeItemText = document.createTextNode(item.title);
  nodeItem.appendChild(nodeItemText);
  if (item.type === 'column-break') {
    nodeItem.classList.add('menu-item--column-break');
  }
  $menu.appendChild(nodeItem);  
});
.menu {
  position: relative;
  padding: 0 16px;
  -webkit-column-count: 4;
     -moz-column-count: 4;
          column-count: 4;
  -moz-column-rule: 1px solid #e2e1e1;
       column-rule: 1px solid #e2e1e1;
  -webkit-column-gap: 32px;
     -moz-column-gap: 32px;
          column-gap: 32px;
}

.menu-item--column-break {
    display: block;
    -webkit-column-break-after: column;
    -moz-break-after: column;
    break-after: column;
    color: red;
}
<div class="container">
  <div class="menu">
  </div>
</div>

为了解决这个问题,我决定采用 js 方法。它并不完全优雅,因为在 js 代码中你需要假设你知道单个菜单项的高度。但这解决了问题,也许它适合您的项目。这个想法是我将菜单的显示更改为一个 flexbox,它将项目放在一列中,但在没有 space 时换行到下一列。现在,为了能够从 space 中 运行 并换行,我们需要两件事:一个固定的菜单高度(这就是我根据提供的项目计算它的原因)以及一个不可见的元素拉伸到 100% 高度(它不适合当前列或下一列,因此它自己创建一个 0px 宽的列,从而充当列分隔符)。看看这个解决方案:

let items = [
  {title: 'Category 1', type: 'menu-item'},
  {title: 'Category 2', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 3', type: 'menu-item'},
  {title: 'Category 4', type: 'menu-item'},
  {title: 'Category 5', type: 'menu-item'},
  {title: 'Category 6', type: 'menu-item'},
  {title: 'Category 7', type: 'menu-item'},
  {title: 'Category 8', type: 'menu-item'},
  {title: 'Category 9', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 10', type: 'menu-item'},
  {title: 'Category 11', type: 'menu-item'},
  {title: 'Category 12', type: 'menu-item'},
  {title: 'Category 13', type: 'menu-item'},
  {title: 'Category 14', type: 'menu-item'},
  {title: 'Category 15', type: 'menu-item'},
  {title: 'Category 16', type: 'menu-item'},
  {title: 'Category 17', type: 'menu-item'},
  {title: 'Category 18', type: 'menu-item'},
  {title: 'Category 19', type: 'menu-item'},
  {title: 'Category 20', type: 'menu-item'},
  {title: 'Category 21', type: 'menu-item'},
];

const $menu = document.querySelector('.menu');

console.log( $menu );
var longestColumnLength = 0;
var currentColumnLength = 0;
var numberOfBreaks = 0;

items.forEach((item) => {
    currentColumnLength++;
    let nodeItem = document.createElement("div");
    nodeItem.classList.add('menu-item');
    let nodeItemText = document.createTextNode(item.title);
    nodeItem.appendChild(nodeItemText);
    if (item.type === 'column-break') {
        nodeItem.classList.add('menu-item--column-break');
        let breaker = document.createElement("div");
       breaker.classList.add('menu-item--column-break-line');
        $menu.appendChild(nodeItem); 
        $menu.appendChild(breaker); 
        longestColumnLength = Math.max(longestColumnLength, 
           currentColumnLength);
        currentColumnLength = 0;
        numberOfBreaks++;
    } else {
        $menu.appendChild(nodeItem); 
    } 
});

var availableNaturalColumnsAtTheEnd = Math.max(1, 4 - numberOfBreaks);
var maxLengthOfRemainingItems = currentColumnLength / 
      availableNaturalColumnsAtTheEnd;
var actualLongestColumn = Math.max(longestColumnLength, 
      maxLengthOfRemainingItems)

$menu.setAttribute("style", "height: " + actualLongestColumn*20 + "px")
.menu {
  position: relative;
  padding: 0 16px;
  display: flex;
  flex-direction: column;
  flex-wrap: wrap;
  height: 200px;
}

.menu-item{
  height: 20px;
}

.menu-item--column-break {
    display: block;
    color: red;
}
.menu-item--column-break-line {
  height: 100%;
  width: 0;
  overflow: hidden;
}
<div class="container">
  <div class="menu">
  </div>
</div>

哦,还有一件事。我不确定您是否真的想显示“---cb---”项目。我让它们显示在解决方案中,但如果你想摆脱它们,你可以通过删除我额外的列分隔符轻松修改代码,而不是让你的“---cb---”项目作为列分隔符。

我在考虑这个问题并想出了另一个解决方案。基本上问题是不支持多列中断,因此目前无法为所有浏览器创建那些仅 css 的固定列和动态列。 因此,我决定将问题一分为二。我根据固定的休息时间将项目分成几组。我假设每个组都将是初学者的一个专栏。然后我看看我有多少列。如果它小于 4(你想要的列数),那么我允许最大的一组动态地分成一列。我继续这样做,直到我达到 4 列的总数 - 无论是固定的,还是动态的,或两者兼而有之。

请参阅下面的代码段。

此外,通过添加、删除或移动中断来播放片段。它应该适用于许多不同的场景。

let items = [
  {title: 'Category 1', type: 'menu-item'},
  {title: 'Category 2', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 3', type: 'menu-item'},
  {title: 'Category 4', type: 'menu-item'},
  {title: 'Category 5', type: 'menu-item'},
  {title: 'Category 6', type: 'menu-item'},
  {title: 'Category 7', type: 'menu-item'},
  {title: 'Category 8', type: 'menu-item'},
  {title: 'Category 9', type: 'menu-item'},
  {title: '---cb---', type: 'column-break'},
  {title: 'Category 10', type: 'menu-item'},
  {title: 'Category 11', type: 'menu-item'},
  {title: 'Category 12', type: 'menu-item'},
  {title: 'Category 13', type: 'menu-item'},
  {title: 'Category 14', type: 'menu-item'},
  {title: 'Category 15', type: 'menu-item'},
  //{title: '---cb---', type: 'column-break'},
  {title: 'Category 16', type: 'menu-item'},
  {title: 'Category 17', type: 'menu-item'},
  {title: 'Category 18', type: 'menu-item'},
  {title: 'Category 19', type: 'menu-item'},
  {title: 'Category 20', type: 'menu-item'},
  {title: 'Category 21', type: 'menu-item'}
];

const $menu = document.querySelector('.menu');

var allGroups = [];
var currentGroup = 0;
allGroups.push({ items: [], columns: 1});

function addGroup($menu, group, numberOfColumns){
 let columnItem = document.createElement("div");
  columnItem.classList.add('menu-group');
  if(numberOfColumns === 1){
   columnItem.classList.add('fixed');
  } else {
   columnItem.classList.add('dynamic-columns');
   var style = '-webkit-column-count: ' + numberOfColumns + ';';
   style += '-moz-column-count: ' + numberOfColumns + ';';
   style += 'column-count: ' + numberOfColumns + ';';
   columnItem.setAttribute('style', style);
  }
  group.forEach((groupItem) => {
   columnItem.appendChild(groupItem);
  });
  $menu.appendChild(columnItem); 
};
var columnsCount = 1;
items.forEach((item) => {
  let nodeItem = document.createElement("div");
  allGroups[currentGroup].items.push(nodeItem);
  nodeItem.classList.add('menu-item');
  let nodeItemText = document.createTextNode(item.title);
  
  nodeItem.appendChild(nodeItemText);
  if (item.type === 'column-break') {
    nodeItem.classList.add('menu-item--column-break');
    //addGroup($menu, currentGroup, 1);
    currentGroup++;
    allGroups.push({ items: [], columns: 1});
    columnsCount++;
  }  
});

var forSorting = [];
allGroups.forEach((item) => { forSorting.push(item); });

while(columnsCount < 4){
 forSorting.sort(function(a, b){
  return (b.items.length/b.columns) - (a.items.length/a.columns);
 });
  forSorting[0].columns++;
  columnsCount++;
}

allGroups.forEach((item) => {
 addGroup($menu, item.items, item.columns);
});
.menu {
  position: relative;
  padding: 0 16px;
  display: flex;
  flex-direction: row;
}

.menu-group:not(:last-child){
  border-right: 1px solid #e2e1e1;
  margin-right: 8px;
}

.menu-group.fixed {
  flex-basis: calc(25% - 8px);
  flex-grow: 0;
  flex-shrink: 0;
}

.menu-group.dynamic-columns {
  flex-grow: 1;
  -moz-column-rule: 1px solid #e2e1e1;
       column-rule: 1px solid #e2e1e1;
}

.menu-item--column-break {
    display: block;
    color: red;
}
<div class="container">
  <div class="menu">
  </div>
</div>