将任意数量的具有不同宽度的元素对齐到带有环绕的网格

Posted

技术标签:

【中文标题】将任意数量的具有不同宽度的元素对齐到带有环绕的网格【英文标题】:Align arbitrary number of elements with different widths to a grid with wrapping 【发布时间】:2018-05-20 11:36:30 【问题描述】:

编辑

我已经接受了下面@user943702 给出的答案。我需要稍微修改它以使用我的 Vue 实现,如下面的 sn-p 所示。

const theElements = [
  name: "ele1",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  , 
    name: 4
  , 
    name: 5
  ]
, 
  name: "ele2",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele3",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele4",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele5",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele6",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele7",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele8",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele9",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele10",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele11",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  , 
    name: 4
  , 
    name: 5
  ]
, 
  name: "ele12",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
];

new Vue(
  el: '#ele-grid',
  data: 
    elements: theElements
  ,
  methods: 
    // find the first grid line excess max
    // return index; -1 means no overflow
    firstoverflowline: function(cols, max) 
      var sum = 0;
      for (var i = 0; i<cols.length; ++i) 
        sum += cols[i];
        if (sum >= max)
          return i;
      
      return -1;
    ,
    // compute max no of columns in grid
    // use by `grid-template-columns:repeat(<max>, max-content)`
    computegridlines: function(container) 
      var cols = getComputedStyle(container).gridTemplateColumns.split(/\s+/).map(parseFloat);
      var x = this.firstoverflowline(cols, parseFloat(getComputedStyle(container).width));
      if (x == -1) return;
      container.style.gridTemplateColumns = `repeat($x, max-content)`;
      this.computegridlines(container);
    ,
    // polyfill `width:max-content`
    maxcontent: function(container) 
      var items = Array.from(container.children);
      for(var i = 0; i < items.length; i++) 
      	var item = items[i];
        item.style.display = "flex";
        item.style.flexFlow = "column";
        item.style.alignItems = "start";
        var max = Array.from(item.children).reduce(function(max,item) 
          var left, right = item.getBoundingClientRect();
          return Math.max(max, right - left);
        , 0);
        item.style.width = `$maxpx`;
      
    ,
    // flex-grid-ify a container
    flexgrid: function(container) 
      container.style.display = `grid`;
      container.style.gridTemplateColumns = `repeat($container.children.length, max-content)`;
      this.computegridlines(container);
      this.maxcontent(container);
    
  ,
  mounted: function() 
    var container = document.getElementById('ele-grid');
    var _this = this;
    this.flexgrid(container);
    window.onresize = function(e)  _this.flexgrid(container); 
  
);
#ele-grid 
  width:100vw;


.ele-card 
  border: 1px solid black;
  background: cyan;
  margin: 5px 3px;

.ele-card .children 
  display: flex;
  flex-wrap: nowrap;
  padding: 5px;

.ele-card .child 
  margin: 0 5px;
  width: 30px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  border: 1px solid black;
  background: magenta;
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.min.js"></script>
<div id="ele-grid">
  <div class="ele-card" v-for="ele in elements" :key="ele.name">
      <div class="element">ele.name</div>
      <div class="children">
        <div class="child" v-for="child in ele.children" :key="child.name">child.name</div>
      </div>
  </div>
</div>

我有未知数量的元素可以有不同的宽度。我想在网格中对齐这些元素,以便它们的左侧在每一列中对齐。此外,我希望元素在窗口变小时换行并保持网格。我在下面的图片中模拟了我想要的东西。

我正在使用 VueJS 2 将元素填充到网格中,并使用 CSS Flexbox 来使用以下 CSS 组织元素。下面是一个 sn-p 示例,说明它现在的功能:

const theElements = [
  name: "ele1",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  , 
    name: 4
  , 
    name: 5
  ]
, 
  name: "ele2",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele3",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele4",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele5",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele6",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele7",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele8",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele9",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele10",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
, 
  name: "ele11",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  , 
    name: 4
  , 
    name: 5
  ]
, 
  name: "ele12",
  children: [
    name: 1
  , 
    name: 2
  , 
    name: 3
  ]
];

new Vue(
  el: '#ele-grid',
  data: 
    elements: theElements
  
);
#ele-grid 
  display: flex;
  flex-wrap: wrap;

.ele-card 
  border: 1px solid black;
  background: cyan;
  margin: 5px 3px;

.ele-card .children 
  display: flex;
  flex-wrap: nowrap;
  padding: 5px;

.ele-card .child 
  margin: 0 5px;
  width: 30px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  border: 1px solid black;
  background: magenta;
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.11/vue.min.js"></script>
<div id="ele-grid">
  <div class="ele-card" v-for="ele in elements" :key="ele.name">
    <div class="element">ele.name</div>
    <div class="children">
      <div class="child" v-for="child in ele.children" :key="child.name">child.name</div>
    </div>
  </div>
</div>

这几乎可以工作;调整窗口大小时,每个元素都有自己的宽度和换行。但是,元素与网格对齐。

我也研究过使用 CSS Grid,但看起来你必须指定每个元素的宽度或列数,这两者我都需要是任意的。

我愿意接受任何使用 CSS 或 javascript(请不要使用 JQuery)的解决方案。我宁愿不包含第 3 方库,但如果它是唯一的选择,我会考虑它。

【问题讨论】:

我删除了我的答案,因为这不能解决您的问题。并且使用脚本将是一个重要的解决方案。如果您检查这 4 个小提琴,它以非固定对齐方式开始,然后手动与边距对齐,一个接一个的小提琴会添加一个边距来设置对齐方式,您会在修复第 4 个时看到,它打破了所有和一个需要一遍又一遍地遍历这些项目,直到最终将它们对齐...jsfiddle.net/ejubqfxf...jsfiddle.net/ejubqfxf/1...jsfiddle.net/ejubqfxf/2...jsfiddle.net/ejubqfxf/3 我在这些演示中使用了固定宽度,所以我们都看到了相同的包装 我在寻找其他东西时发现了这个...***.com/questions/24194537/… ...它可能对您有帮助 【参考方案1】:

编辑:

正如@user943702 所指出的,我们可以利用max-content 属性来删除每列中的多余空格(不要将此属性与解释中的属性混淆,尽管它是基于每个元素的宽度值,并且这是每列的基础)

对于空间分布:有一个方便的属性叫做justify-content,我选择将其设置为中心,除其他值外,您可以将其设置为:

space-between; /* The first item is flush with the start,the last is flush with the end */
space-around;  /* Items have a half-size space on either end */
space-evenly;  /* Items have equal space around them */
stretch;       /* Stretch 'auto'-sized items to fitthe container */

在进入脚本之前,有几点注意事项:

您可以通过以下方式将其设置为仅使用 css 进行响应式更改:@media query 每个宽度一个并完成它,但是各个元素也具有任意宽度,因此我将使用 JavaScript

编辑:这是一个使用 CSS media query 方法的脚本,请注意,您尝试将其自定义为不同的设备宽度的次数越多,当单个元素的宽度意外更改时,您就越有可能被捕获。

const theElements = [  name: "ele1",  children: [    name: 1  ,     name: 2  ,     name: 3  ,     name: 4  ,     name: 5  ],   name: "ele2",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele3",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele4",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele5",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele6",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele7",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele8",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele9",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele10",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele11",  children: [    name: 1  ,     name: 2  ,     name: 3  ,     name: 4  ,     name: 5  ],   name: "ele12",  children: [    name: 1  ,     name: 2  ,     name: 3  ]];

new Vue(
  el: '#ele-grid',
  data: 
    elements: theElements
  
);
@media (min-width: 1020px) 
	#ele-grid 
		display:grid;
		grid-template-columns:repeat(5, 1fr); 	
        justify-content: center;
	

@media (min-width:400px) and (max-width: 1020px) 
	#ele-grid 
		display:grid;
		grid-template-columns:max-content max-content max-content; 	
	

@media (max-width: 400px) 
	#ele-grid 
		display:grid;
		grid-template-columns:max-content; 	
	

.ele-card 
  margin: 5px 3px;

.ele-card .children 
  display: flex;
  flex-wrap: nowrap;
  padding: 5px;

.ele-card .child 
  margin: 0 5px;
  width: 30px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  border: 1px solid black;
  background: magenta;

.wrapper
	border: 1px solid black;
  background: cyan;
  display:inline-block;
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.28/vue.min.js"></script>

<div id="ele-grid">
  <div class="ele-card" v-for="ele in elements" :key="ele.name">
  	<div class="wrapper">
	    <div class="element">ele.name</div>
	    <div class="children">
	      <div class="child" v-for="child in ele.children" :key="child.name">child.name</div>
	    </div>
	  </div>
	</div>
</div>
要获得所需的宽度,有一个出色的 -moz-max-content 属性,不幸的是,其他浏览器尚不支持它,所以我附加了一个子包装器并使其 display:inline-block 具有预期的行为 我使用的是CSS grid layout,您可以改用css列或垂直flexs,但元素会从上到下对齐,从而改变整个布局。

那是针对 css,针对 JavaScript:

简而言之,此脚本采用具有最大列数的布局(此处可以增加 10 列),并查看它是否适合不滚动,如果不减少。 在此脚本中,元素使用调整大小事件进行响应。

const theElements = [  name: "ele1",  children: [    name: 1  ,     name: 2  ,     name: 3  ,     name: 4  ,     name: 5  ],   name: "ele2",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele3",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele4",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele5",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele6",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele7",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele8",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele9",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele10",  children: [    name: 1  ,     name: 2  ,     name: 3  ],   name: "ele11",  children: [    name: 1  ,     name: 2  ,     name: 3  ,     name: 4  ,     name: 5  ],   name: "ele12",  children: [    name: 1  ,     name: 2  ,     name: 3  ]];

new Vue(
  el: '#ele-grid',
  data: 
    elements: theElements
  
);
function resizeHandler()
	colStart=10; 		// max number of columns to start with
	allCards= document.getElementsByClassName('wrapper');
	totalWidth=0;
	maxWidTab=[];
	for (i=colStart;i>0;i--)
		for(j=0;j<i; j++)									//initializing and resetting
			maxWidTab[j]=0;
		
		for (j=0; j<allCards.length; j++)
			cellWidth=parseInt(getComputedStyle(allCards[j]).width);		//parseInt to remove the tailing px
			maxWidTab[j%i]<cellWidth?maxWidTab[j%i]=cellWidth:'nothing to be done';
		
		for(j=0;j<i; j++)									//sum to see if fit
			totalWidth+=maxWidTab[j]+2+6		//borders and margins
		
		if (totalWidth<innerWidth)
			grEl = document.getElementById("ele-grid");
			grEl.style.gridTemplateColumns="repeat("+i+", max-content)";
			/*console.log(i);*/
			break;
		else
				totalWidth=0;							//resetting
		
	

	window.addEventListener("resize",resizeHandler);
	document.addEventListener ("DOMContentLoaded",resizeHandler);
#ele-grid 
	display:grid;
    justify-content: center;
	grid-template-columns:repeat(10, max-content); 	/* starting by 10 columns*/

.ele-card 

  margin: 5px 3px;

.ele-card .children 
  display: flex;
  flex-wrap: nowrap;
  padding: 5px;

.ele-card .child 
  margin: 0 5px;
  width: 30px;
  height: 30px;
  text-align: center;
  line-height: 30px;
  border: 1px solid black;
  background: magenta;

.wrapper
	border: 1px solid black;
  background: cyan;
  display:inline-block;


</style>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.28/vue.min.js"></script>

<div id="ele-grid">
  <div class="ele-card" v-for="ele in elements" :key="ele.name">
  	<div class="wrapper">
	    <div class="element">ele.name</div>
	    <div class="children">
	      <div class="child" v-for="child in ele.children" :key="child.name">child.name</div>
	    </div>
	  </div>
	</div>
</div>

【讨论】:

这与我正在寻找的非常接近。我遇到的一个问题是元素之间的空间被拉伸以填充整个屏幕宽度,除了该行的第一个和第二个元素之间。我真的不希望它填满整个屏幕宽度。如果是这样,我希望元素之间的空间相等。 @sorayadragon 非常合法!相应地更新了答案 我不接受这个答案有几个原因:1.您在每张卡片中添加了一个“.wrapper”div,因此它不再符合我的实现 2.如果窗口很宽足以容纳超过 10 列,它没有。我知道我可以更改号码,但我不喜欢这样硬编码。【参考方案2】:

CSS grid-template-columns 确实支持内容感知值 max-content。唯一的问题是应该有多少列。

我编写了一个算法来探测最大列数。实现涉及JS,需要浏览器支持CSS Grid。可以在here找到演示。 (我使用 Pug 创建与您相同的源结构,并且样式也与您的相同,以便我们可以专注于 JS 面板,实现)。

在演示中,更改视口大小将重新排列网格项目。您可以通过调用flexgrid(container) 在其他有趣的时刻手动触发重新流动,例如异步加载项目然后重新流动。只要源结构保持不变,就允许更改项目的维度属性。

这是算法

Step1) 将容器设置为网格格式化上下文,将所有网格项排成一行,将每列宽度设置为max-content

|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggg|hhh|iii|

Step2) 找到第一条溢出网格线

|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggg|hhh|iii|
                  ^overflowed

步骤 3) 将 grid-template-columns 减少到,在我们的例子中,为 3。 由于grid-row 默认为auto,CSS 引擎布局一个网格项 当它超出最后一个列网格线时,在下一行。我叫这个 “包装”。此外,由于grid-template-columns:max-content,网格项会自动扩展(例如,“ddd”会扩展为第一列最宽内容的长度)

|---container---|
|aaaaa|bbb|ccc|
|ddd  |eee|fff|
|ggggg|hhh|iii|

由于所有列网格线都位于“内部”容器中,因此我们已经完成了。在某些情况下,在“包装”之后会引入新的溢出网格线,我们需要重复步骤 2 和 3,直到所有网格线都位于“内部”容器中,例如

#layout in one row
|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggggg|hhhhh|iii|

#find the first overflowed grid line
|---container---|
|aaaaa|bbb|ccc|ddd|eee|fff|ggggggg|hhhhh|iii|
                  ^overflowed

#reduce `grid-template-columns`
|---container---|
|aaaaa  |bbb  |ccc|
|ddd    |eee  |fff|
|ggggggg|hhhhh|iii|

#find the first overflowed grid line
|---container---|
|aaaaa  |bbb  |ccc|
|ddd    |eee  |fff|
|ggggggg|hhhhh|iii|
                  ^overflowed

#reduce `grid-template-columns`
|---container---|
|aaaaa  |bbb  |
|ccc    |ddd  |
|eee    |fff  |
|ggggggg|hhhhh|
|iii    |

#find the first overflowed grid line
#None, done.

【讨论】:

很好的解释+1 这是一个很好的答案,有很好的解释!我需要稍微修改它以使用我的 Vue 实现,但它是最小的。

以上是关于将任意数量的具有不同宽度的元素对齐到带有环绕的网格的主要内容,如果未能解决你的问题,请参考以下文章

我如何垂直对齐具有不同高度的网格元素? [重复]

是否可以有一个具有固定高度但可变宽度的元素的列流网格?

使用CSS网格将所有项目水平对齐到左侧

具有“空白:nowrap”的元素的宽度不适应子内容

使用适当的包装将元素左右对齐

将元素与使用 display: table 的容器底部对齐