查看原文
其他

【第2752期】CSS 有了:has伪类可以做些什么?

XboxYan 前端早读课 2022-10-14

前言

通过两篇用 CSS 来偷资料了解到选择器的威力,那今天就来看看最近刚被浏览器支持的:has。今日前端早读课文章由阅文 @XboxYan 分享,公号:前端侦探授权。

@XboxYan,阅文体验设计部,专注用户体验相关,热爱 CSS,热爱原生,欢迎关注 github:https://github.com/XboxYan

正文从这开始~~

【第2751期】用 CSS 来偷数据 - CSS injection(下)

【第2750期】用 CSS 来偷数据 - CSS injection(上)

在 fb 上看到的一段来自 @Paul Li 的分享:

CSS > :has()

一个期待许久的 CSS pseudo class > :has () 总算可以在 Chrome / Safari 上使用了。

CSS selector 的编写一向都是由左至右去 style 符合最右端条件的 elements,有了这个 pseudo class 后,web developers 就可以透过它去反推其 child elements 是否符合条件,一旦符合,便可以进行样式调整。可以说是便利非常,亦可以减少不必要的 JavaScript 辅助运算。

如范例所示,笔者依照当前 HTML 结构以及 :has() 来 style loading sign,让 loading sign 的 block-size 依照条件的设置有不同的呈现,由于尚有部分浏览器不支持(Firefox),所以可以搭配 supports 来做判断保护。

好了,上面是前戏,文章从这开始了~~

相信大家最近对:has 都有所耳闻,规范提及了那么久,却迟迟没有动静,最近浏览器终于开始支持了~🎉🎉

:has 伪类是一个非常强大的伪类,强大到难以置信,可以做很多梦寐以求的事情,很多以前只能更改 dom 结构 或者只能用 JS 才能实现的功能现在也能纯 CSS 实现了,一起看看吧

一、简单介绍一下 :has

:has 伪类的语法非常简单,表示满足一定条件后,就会匹配该元素。

例如,下面的选择器只会匹配直接包含 img 子元素的 a 元素:

a:has(> img)

再例如,下面的选择器只会匹配其后紧跟着 p 元素的 h1 元素

h1:has(+ p)

以我个人的理解来看,去除:has() 后,剩下的选择器仍然是完整的

a>img

加上:has() 后,可以选中最前面的元素 a。

好了,语法其实就这么多,估计没啥吸引力,关键是实际应用。下面通过几个实例来感受一下:has 伪类的强大魅力~

温馨提醒:兼容性要求需要 Chrome 101+,并且开始实验特性(105 + 正式支持),Safari 15.4+,Firefox 官方说开启实验特性可以支持,但是实测并没有(???)

二、表单元素必填项

先来看一个简单例子,下面有一个表单元素,有一些是必填项

<form>
<item>
<label>用户名</label>
<input required>
</item>
<item>
<label>备注</label>
<input>
</item>
</form>

现在可以通过:has 在必填项的前面加上红色的星号

label:has(+input:required)::before{
content: '*';
color: red;
}

这个应该还比较好理解,通过:has 和 + 可以选中满足条件的 label,然后再生成::before 伪元素。如果是在以前,可能需要手动添加类名,或者改变 html 的书写顺序

你也可以访问线上完整 demo:has+required (codepen.io) 或者 has+required (runjs.work)

  • has+required(codepen.io): https://codepen.io/xboxyan/pen/yLjyopY

  • has+required(runjs.work): https://runjs.work/projects/5819b7bc43474966

三、拖拽指定区域

有些时候列表需要有拖拽功能,但为了拖拽体验,不影响列表内部操作,可能需要指定小部分区域可拖拽,例如

HTML 结构如下

<div class="content">
<div class="item">列表<span class="thumb"></span></div>
<div class="item">列表<span class="thumb"></span></div>
<div class="item">列表<span class="thumb"></span></div>
</div>

现在我们希望在 hover 时出现拖拽手柄,按住拖拽手柄才可以拖拽,看着好像非常麻烦,但是现在借助:has 伪类可以轻易实现,关键 CSS 如下

.thumb{
/**/
opacity: 0
}
.item:hover .thumb{
opacity: 1;
}
.item:has(.thumb:hover){
-webkit-user-drag: element;
}

这里的:has 表示当.thumb 处于:hover 状态时选中该元素,从而给.item 添加可拖拽属性,效果如下

你也可以访问线上完整 demo:drag_thumb (codepen.io) 或者 drag_thumb (runjs.work)

  • drag_thumb(codepen.io): https://codepen.io/xboxyan/details/QWrwMaL

  • drag_thumb(runjs.work): https://runjs.work/projects/502506f18ee34b6e

四、多层级 hover

再来看一个例子,早在四年前我就提到过的 ,现在终于有解了~

是这样的,有一个多层级的结构,例如

<div class="box-1">
<div class="box-2">
<div class="box-3"></div>
</div>
</div>

如果给 div 添加 hover 样式

div:hover{
outline:4px dashed rebeccapurple
}

效果是这样

可以看到,当 hover 到里层元素时,外层元素也触发了 hover 样式。这有点像 JS 中的冒泡效果,那如何让 hover 的时候只触发当前的元素呢?也就是排除掉他的父级元素,没错,:has 可以很好的解决这个问题

div:not(:has(:hover)):hover{
outline:4px dashed rebeccapurple
}

是不是越来越绕了?别急,我们拆解分析一下,div:has(:hover) 表示有子元素正处于 hover 的 div,比如当 hover 到 box-3 时,div:has(:hover) 选中的就是除 box-3 以外的两个父级,然后加上:not 就刚好反过来,只选中 box-3 本身,可以理解吗?这个可以下来多试试,实际效果如下

你也可以访问线上完整 demo:CSS hover (codepen.io) 或者 CSS hover (runjs.work)

  • CSS hover(codepen.io): https://codepen.io/xboxyan/pen/eYrmEyd

  • CSS hover(runjs.work): https://runjs.work/projects/2a7e819e473c47af

在一些可视化拖拽平台,各种嵌套的组件中会很有作用

五、评星组件

这个功能也是非常适合用:has 来实现的,HTML 结构如下

<star>
<input name="star" type="radio">
<input name="star" type="radio">
<input name="star" type="radio">
<input name="star" type="radio">
<input name="star" type="radio">
</star>

简单修饰一下

star{
display: flex;
}
star [type="radio"]{
appearance: none;
width: 40px;
height: 40px;
margin: 0;
cursor: pointer;
background: #ccc;
transition: .3s;
-webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 512 512'%3E %3Cpath d='M462.3 62.6C407.5 15.9 326 24.3 275.7
76.2L256 96.5l-19.7-20.3C186.1 24.3 104.5 15.9 49.7 62.6c-62.8 53.6-66.1 149.8-9.9 207.9l193.5 199.8c12.5 12.9 32.8 12.9 45.3 0l193.5-199.8c56.3-58.1
53-154.3-9.8-207.9z'%3E%3C/path%3E %3C/svg%3E") center / 80% no-repeat;
}

效果如下

下面要做交互功能,当:hover 或者:checked 时,当前元素和当前元素之前的元素都触发选中。

在之前,由于只有后置兄弟选择器~,所以必须要将 dom 元素更改顺序,然后通过其他方式在视觉上再翻转过来。现在有了:has,这些奇技淫巧都可以说拜拜了,实现如下

star [type="radio"]:hover,
star [type="radio"]:has(~:hover),
star:not(:hover) [type="radio"]:checked,
star:not(:hover) [type="radio"]:has(~:checked)
{
background: orangered;
}

相信不算太复杂,[type="radio"]:has(~:hover) 表示选中当前 hover 元素之前的元素,所以可以轻易的实现评分的效果

你也可以访问线上完整 demo:CSS star (codepen.io) 或者 CSS rate (runjs.work)

  • CSS star (codepen.io): https://codepen.io/xboxyan/pen/abGzyEw

  • CSS rate(runjs.work): https://runjs.work/projects/b1ad1cf84b314dfd

六、日期范围选择

如果说上面这些例子有其他代替方案,或者说用一点点 JS 也能实现,那下面来一个重磅级的案例。在以前,就算靠 JS 也会有一些麻烦,但是有了:has,一切都变得简单了~

假设 HTML 结构如下

<div class="date">
<span>1</span>
<span>2</span>
<span>3</span>
...
<span>30</span>
<span>31</span>
</div>

这部分交互有两个部分,一个是鼠标滑过,还有一个是选中。

我们先看选中的功能,当有两个元素被选中时,这两个元素之间的元素都会匹配上,假设选中的类名是 select

<div class="date">
<span>1</span>
<span>2</span>
<span class="select">3</span>
...
<span class="select">30</span>
<span>31</span>
</div>

那么,如何让这一片区域的元素都匹配上呢?答案就是通过:has 找到 select 之前的元素,再结合~匹配之后的元素,两者结合就可以匹配到中间的元素了,具体实现如下

.select,
.select~span:has(~.select)
{
background-color: blueviolet;
color: #fff;
}

效果如下

然后是 hover 的效果,假设有一个已经被选中了

<div class="date">
<span>1</span>
<span>2</span>
<span class="select">3</span>
...
<span>30</span>
<span>31</span>
</div>

现在需要再鼠标滑过的时候,将鼠标的终点和已选中的范围都匹配上,这个就稍微有些复杂了,我们需要考虑鼠标在已选中元素之前还是之后,分别用:has 进行判断,实现如下

span:hover~span:has(~.select),
.select~span:has(~:hover)

{
background-color: blueviolet;
color: #fff;
}

是不是有些晕了?第一条表示鼠标在已选中之前,匹配当前 hover 之后、.selelct 之前的元素, 第二条表示已选中之后,匹配.selelct 之后、hover 之前的元素,实际效果如下

还有一个问题,需要区分一下选中两个和只选中一个的情况,因为两个表示区间选择已经完成,此时 hover 不会有效果,借助:has 伪类,可以很轻易的区分子元素的个数,如下

.date:not(:has(.select~.select)){
/*匹配到没有两个.select的父级*/
}

.select~.select 表示选中.select 后面的.select,也就是表示至少有两个.select,然后通过:has 就可以区分这两种情况了

.date:not(:has(.select~.select)) .select,
.date:not(:has(.select~.select)) span:hover
{
background-color: transparent;
color: inherit;
outline: 2px solid blueviolet;
outline-offset: -2px;
}

.date:not(:has(.select~.select)) span:hover~span:has(~.select),
.date:not(:has(.select~.select)) .select~span:has(~:hover)

{
background-color: blueviolet;
color: #fff;
}

元素的选中时通过 JS 实现的,这时候的 JS 完全就只是工具人了,和视觉完全不相干,只需要记录选中的元素,逻辑极其简单,如下

date.addEventListener('click', ev => {
const current = date.querySelectorAll('.select');
if (current.length == 2) {
current.forEach(el => {
el.classList.remove('select')
})
}
ev.target.classList.add('select')
})

然后就可以得到这样的效果了

你也可以访问线上完整 demo:CSS date-range (codepen.io) 或者 CSS date-range (runjs.work)

  • CSS date-range(codepen.io): https://codepen.io/xboxyan/details/dyePzVd

  • CSS date-range(runjs.work): https://runjs.work/projects/85ba26f3ff18414b

等待时机成熟,xy-ui 中的 也会跟进使用这一方案~

七、别等了,现在就学起来

以上就是我在研究:has 伪类后,第一时间所想到的一些应用场景,这么好用的伪类,还不学一下吗?

当然其作用远不止这些,可以这么讲,以前需要考虑 dom 顺序的场景都可以解决了,以后的 dom 将更加语义化,大部分交互状态伪类都可以很好地结合在一起,80% 以上交互相关的 JS 代码都可以去掉了,JS 可以更加安心的去做自己该做的事情了,比如数据转换,业务逻辑等等。

关于本文
作者:@XboxYan
原文:https://mp.weixin.qq.com/s/qVfdHy8QjZFVjEscAZQYXw

关于【选择器】相关推荐,欢迎读者自荐投稿,前端早读课等你来。+v:zhgb_f2er

【第1754期】CSS第四级选择器

【第1067期】CSS选择器从右向左的匹配规则

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存