CSS - 借助元素数量选择器实现“向前选择”

617 阅读4分钟

原文地址:# CSS - 元素数量选择器

举个栗子

场景一

今天在给 hexo-theme-zhaoo文章目录 (toc),有个需求:当目录不存在时,不显示标题。

文章目录

抽象出 DOM 结构:

<aside class="toc-wrap">
  <h3 class="toc-title">文章目录:</h3>
  <ol class="toc">...</ol>
</aside>

最直接的思路是匹配 .toc,若该元素不存在,则将它前面的 .toc-title 隐藏。但是 CSS 没有向前选择,因为这样会引起回溯。(详见:# 如何给 W3C 组织提关于 Web 标准的建议? - 贺师俊的回答

需要想个办法曲线救国,其实借助它们的公共父元素 .toc-wrap 即可解决,若 .toc-wrap 只有一个子元素则将它隐藏。如下:

.toc-wrap
  & > :only-child
    display none

🌰 很简单,却值得思考一下,CSS 真的不能 “向前选择” 嘛?

场景二

在构建 栅格化 布局的时候,早期的 BootStrap 使用了大量的重复代码,类似这样:

@media (min-width: 768px) {
  .col-md-1 { flex: 0 0 8.33333%; max-width: 8.33333%; }
  .col-md-2 { flex: 0 0 16.6667%; max-width: 16.6667%; }
  .col-md-3 { flex: 0 0 25%; max-width: 25%; }
  .col-md-4 { flex: 0 0 33.3333%; max-width: 33.3333%; }
  .col-md-5 { flex: 0 0 41.6667%; max-width: 41.6667%; }
  .col-md-6 { flex: 0 0 50%; max-width: 50%; }
  .col-md-7 { flex: 0 0 58.3333%; max-width: 58.3333%; }
  .col-md-8 { flex: 0 0 66.6667%; max-width: 66.6667%; }
  .col-md-9 { flex: 0 0 75%; max-width: 75%; }
  .col-md-10 { flex: 0 0 83.3333%; max-width: 83.3333%; }
  .col-md-11 { flex: 0 0 91.6667%; max-width: 91.6667%; }
  .col-md-12 { flex: 0 0 100%; max-width: 100%; }
}
@media (min-width: 992px) {
  .col-lg-1 { flex: 0 0 8.33333%; max-width: 8.33333%; }
  .col-lg-2 { flex: 0 0 16.6667%; max-width: 16.6667%; }
  .col-lg-3 { flex: 0 0 25%; max-width: 25%; }
  .col-lg-4 { flex: 0 0 33.3333%; max-width: 33.3333%; }
  .col-lg-5 { flex: 0 0 41.6667%; max-width: 41.6667%; }
  .col-lg-6 { flex: 0 0 50%; max-width: 50%; }
  .col-lg-7 { flex: 0 0 58.3333%; max-width: 58.3333%; }
  .col-lg-8 { flex: 0 0 66.6667%; max-width: 66.6667%; }
  .col-lg-9 { flex: 0 0 75%; max-width: 75%; }
  .col-lg-10 { flex: 0 0 83.3333%; max-width: 83.3333%; }
  .col-lg-11 { flex: 0 0 91.6667%; max-width: 91.6667%; }
  .col-lg-12 { flex: 0 0 100%; max-width: 100%; }
}

在使用 CSS 预处理器 后可以简化成下面这样,但是编译后依然存在大量重复代码。

for $i in 1 .. 12
  .col-{$i}
    col-attr round(($i * 100 / 12) %, 6)
@media (min-width 576px)
  for $i in 1 .. 12
    .col-sm-{$i}
      col-attr round(($i * 100 / 12) %, 6)
@media (min-width 768px)
  for $i in 1 .. 12
    .col-md-{$i}
      col-attr round(($i * 100 / 12) %, 6)
@media (min-width 992px)
  for $i in 1 .. 12
    .col-lg-{$i}
      col-attr round(($i * 100 / 12) %, 6)
@media (min-width 1200px)
  for $i in 1 .. 12
    .col-xl-{$i}
      col-attr round(($i * 100 / 12) %, 6)

灵魂拷问,CSS 真的不适合处理 “重复元素” 的场景嘛?

前置知识

一些 CSS3 选择器:

  • > 子元素选择器
  • + 相邻元素选择器
  • ~ 兄弟元素选择器

一些 CSS3 伪类:

  • :only-child 父元素的唯一子元素
  • :first-child 父元素的第一个子元素
  • :last-child 父元素的最后一个子元素
  • :nth-child(n) 父元素的第 N 个子元素
  • :nth-last-child(n) 父元素的倒数第 N 个子元素
  • :nth-child(xn+y) 父元素的第 xN + y 个子元素
  • :nth-last-child(xn+y) 父元素的倒数第 xN + y 个子元素

化学反应

结合以上的 选择器伪类 可以产生一些有趣的 “化学反应”,基于元素的数量来匹配样式。

DOM 结构如下:

<ul>
  <li>1</li>
  <li>2</li>
  <li>3</li>
  ...
</ul>

André Luís

André Luís 方案如下:

/* 只有一个元素时 */
ul>li:nth-child(1):nth-last-child(1) { width: 100%; }  /* 匹配第一个元素 */
ul>li:only-child { width: 100%; }  /* 匹配第一个元素 */
/* 有且仅有两个元素时 */
ul>li:nth-child(1):nth-last-child(2),  /* 匹配第一个元素 */
ul>li:nth-child(2):nth-last-child(1) {  /* 匹配第二个元素 */
	width: 50%;  /* 合起来就是匹配所有元素 */
}
/* 有且仅有三个元素时 */
ul>li:nth-child(1):nth-last-child(3),
ul>li:nth-child(2):nth-last-child(2),
ul>li:nth-child(3):nth-last-child(1) {
	width: 33.3333%;
}

Clever lists with CSS3 selectors

Lea Verou

上述方案存在一个问题,当元素数量过多时,还是存在大量的重复选择器。升级版的 Lea Verou 方案只需两行固定的选择器即可解决。

/* 只有一个元素时 */
ul>li:first-child:nth-last-child(1) {
	width: 100%;
}
/* 有且仅有两个元素时 */
ul>li:first-child:nth-last-child(2),
ul>li:first-child:nth-last-child(2) ~ li {
	width: 50%;
}
/* 有且仅有三个元素时 */
ul>li:first-child:nth-last-child(3),  /* 匹配第一个元素 */
ul>li:first-child:nth-last-child(3) ~ li {  /* 匹配除第一个元素外的所有元素 */
	width: 33.3333%;
}

Styling elements based on sibling count

再改进

不确定元素数量时:

/* 大于三个元素时 */
ul>li:first-child:nth-last-child(n+3),
ul>li:first-child:nth-last-child(n+3) ~ li {
  width: calc(100% / n);
}

总结

通过上述方法即可解决 向前选择重复元素 这两个问题,遇到实际场景时借助父元素灵活运用即可。