写这篇文章是因为,昨天在组里临时做了个分享,觉得还蛮有意思的,所以想记录下来。分享内容是讲 CSS Counter 的,从怎么使用它,到它的实际使用价值,以及一些延伸的用法。那我们直接进入正题。
怎么使用 CSS Counter
首先要明确的是,CSS Counter 从一开始就是为了解决列表项的序号展示需求而设计的。功能上,它能够用来统计元素的个数,并通过赋给 CSS 的 content 属性来展示在页面上。
那么,如果让你设计一个计数器,你会怎么设计呢?我觉得应该至少包含这几个部分: 1. 计数目标:你要统计什么 2. 计数增量:目标没出现一次,加多少 3. 清零:重置计数值 4. 计数值:当前出现的目标数量 * 计数增量
所以用 JS 代码可以这样写:
class Counter {
count = 0 // 计数值
constructor(target, increment) {
// 计数算法
if (appear(target)) {
count += increment
}
}
// 清零
reset() {
this.count = 0
}
}
换成 CSS 的话,我们首先需要创建一个 Counter:
.reset {
counter-reset: test;
}
“test” 表示计数器的标识。有人会疑问counter-reset
不是代表重置的意思吗?对,它确实是重置计数器值的意思,但同时也起了创建一个计数器的作用。所以这行代码的完整意思是:创建一个计数器,它的标识是 “test”,并且在遇到 .reset 元素时,重置计数值。
接下来需要指定计数对象和每次计数的增量。同样的,CSS 在设计上将这两个功能合并成了一个属性。因为是为了统计“元素”的个数,所以在 CSS 中,计数目标是代表某个元素的选择器,比如:
.target {}
然后可以通过设置 counter-increment 属性来表示,计数器 test 每遇到一个 .target 元素就加1:
.target {
// 第二个参数可以省略,默认为 1。
counter-increment: test 1;
}
在 js 中,我们通过 counter.count 来获得计数值。而在 css 中,我们可以通过 counter() 方法来获取,并赋给 content 属性就可以在节点中展示出来了:
.count:before {
content: counter(test);
}
另外还可以通过 counters 方法,获得嵌套的计数器的计数拼接值。这里的嵌套关系来源于元素的父子关系,如果两个设置了计数器的元素是父子关系,那么这两个计数器就是嵌套关系。代码示例:
.count:before {
// 嵌套值会展示成这样的格式 "1-2-3"
content: counters(test, "-");
}
更多 CSS Counter 的语法和使用方式,可以参考MDN 文档,这里就不再赘述。
CSS Counter 的实用价值
平时如果遇到有序列表的需求,序号都是通过模板硬编码实现的,比如:
<h2>《三十六计》</h2>
<ol className="category">
{chapters.map((chapter, ci) => (
<>
<li className="title">
{String.fromCharCode(97 + ci)}. {chapter.title}
{chapter.sections && (
<ol className="section">
{(chapter.sections || []).map((section, si) => (
<li className="title">
{String.fromCharCode(97 + ci)}-
{String.fromCharCode(97 + si)}. {section.title}
</li>
))}
</ol>
)}
</li>
</>
))}
</ol>
效果:
这样就可以渲染出一个有序列表。对于类似的列表展示需求,大多数情况我不会想到使用 CSS 来实现,这里主要原因可能是语言上下文的切换成本 > 直接硬编码的成本。但是如果对于一些特殊需求,硬编码的成本 > 语言上下文切换的成本,我们就可以尝试使用 CSS Counter 来实现了。比如产品要求需要不再是“a, b, c, d…”而是 “壹,贰,叁,厮...”,这时候我们可能需要实现一个中文计数序号编码算法。可能有些同学说实现一个也不是很难啊?是的,但这取决于开发的知识储备,如果之前没有实现过的,成本肯定是大于直接使用 CSS 来实现的。使用 CSS,我们只需要设置一下counter 或 counters 的最后一个参数即可:
counter(test, 'simp-chinese-formal')
另外 CSS 还实现了非常多其他的序号样式,可以参考:list-style-type - CSS(层叠样式表) | MDN 比起实现计数序号编码算法,CSS 只用加个参数,显然要简单很多。 浏览器兼容性可参考:list-style-type - CSS(层叠样式表) | MDN 可以看到,很多都是可以在大多数生产环境中被使用了。所以,如果产品需求中出现了多种编码方式的序号,就可以考虑使用 CSS Counter 来实现。
下面再来看下另外一个功能需求 — 内容防复制。
如上图所示,我们在选择列表的时候,可以发现,序号是没有被选上的。并且也无法被复制。之所以有这样的表现,是因为序号的内容是通过 css 的 content 属性来生成的,实际的 DOM 节点的 content 属性中,并没有内容。那么,我们是不是可以通过相同的技术来实现内容防复制功能呢?本质上,我们是利用了 CSS content 属性带来的特性。比如这样写,我们就可以将章节内容“隐藏”起来:
<h2>《三十六计》</h2>
<ol className="category category--css">
{chapters.map((chapter, ci) => (
<>
<li className="title" data-content={chapter.title}>
{chapter.sections && (
<ol className="section">
{(chapter.sections || []).map((section, si) => (
<li className="title" data-content={section.title}>
</li>
))}
</ol>
)}
</li>
</>
))}
</ol>
.category--css .title:before {
content: counters(test, "-", simp-chinese-formal) ". " attr(data-content);
}
我们再回到内容防复制功能本身,要实现这个功能并不是只有这一种方式,比如通过 -webkit-user-select: none;
。虽然这在大多数环境中是有效的,但是在safari 中会遇到一个问题 -- 虽然看起来没被选中,但是确可以被复制。当你选择的区域包含可复制和不可复制的内容,那么在 safari 中,不可复制的区域就会被隐形选中,复制的内容中也会包含不可复制的内容,如图:
所以我们必须将整个页面设置为 -webkit-user-select: none 才可以使得内容不可被复制。相比起来,使用 content 来实现不可复制,是一个兼容性更强的方案。
其实 content 内容生成技术,已经被使用在了很多场景里,比如使用 attr 方法展示图片 hover 提示、字体图标等。网上可以搜到很多资料,这里也不再展开。
总结
最后总结一下,如果产品需求里要求展示多种编码方式的序号,可以考虑使用 CSS Counter 来生成序号;如果想要实现文本不可复制功能,CSS content 生成技术是一个兼容性相对较好的方案。
附完整代码示例:
cranky-chaplygin-fltup - CodeSandboxcodesandbox.io