前言
最近做项目时碰到这么一个需求:
这有点类似于手风琴效果,但不一样的是很多手风琴效果是同一时间内只能有一个展开,而这个是各个部分独立的,你展不展开完全不会影响我的展开与否。其实这种效果简直再普遍不过了,网上随便一搜就出来一大堆。但不一样的是,我在接到这个需求的时候突然想起来很久以前看过张鑫旭大佬的一篇文章,模糊的记得那篇文章里说过有个什么很方便的 CSS 属性能够实现这一效果,不用像咱们平时实现的那些展开收起那样写很多的代码,于是就来到他的博客里面一顿搜,找了半天终于发现原来是我记错了,并不是什么 CSS3 属性,而是 HTML5 标签!
details
想要非常轻松的实现一个收拉效果,需要用到三个标签,分别是:<details>
、<summary>
以及随意
…
随意是什么意思?意思是什么标签都可以?
咱们先只写一个<details>
标签来看看页面上会出现什么:
<details></details>
运行结果:
可以看到非常有意思的一个现象:我们明明什么文字都没有写,但页面上却出现了详细信息
这四个字,因为如果你在标签里没有写<summary>
的话,浏览器会自动给你补上一个<summary>详细信息</summary>
,那有人可能奇怪了,怎么补的是中文呢?那老外不写<summary>
的话也会来一个<summary>详细信息</summary>
?其实是这样:
开个玩笑,其实是根据你当前操作系统的语言来判断的,要是你把系统语言改成其它语言的话出现的就不再是'详细信息'这几个中文字符了。
那如果我们在<details>
标签里写了<summary>
呢?
<details>
<summary>公众号:</summary>
</details>
运行结果:
可以看到<summary>
里面的文字就会在三角箭头旁边的标题位置展示出来,可是我们展开三角箭头发现里面什么内容也没有,那么内容写在哪呢?
只需写在<summary>
的后面就可以了,那是不是还要写个固定标签呢?比如什么<describe>
之类的,其实在<summary>
之后无论写什么标签都可以,当然必须得是合法的 HTML 标签啊,比如我们写个<h1>
标签来试试看:
<details>
<summary>公众号:</summary>
<h1>前端学不动</h1>
</details>
运行结果:
再换个别的标签试试:
<details>
<summary>公众号:</summary>
<button>前端学不动</button>
</details>
运行结果:
看!我们仅用了三个标签就完成了一个最简单的收拉效果!以前在网上看到类似的效果要么就是 getElementById 获取到 DOM 元素,然后添加 onclick 事件控制下方元素的 style 属性,要么就是纯 CSS 实现,写几个单选按钮配合兄弟选择器来控制后方元素的显隐,抑或是 CSS 与 JS 相结合来实现的,但仅靠 HTML 标签来实现这一效果还是非常清新脱俗的!并且十分简洁、非常节约代码量、也更加直观易于理解。
深入测试
既然<summary>
标签后面写什么都行,那么可不可以写很多个标签呢?我们来测试一下:
<details>
<summary>公众号:</summary>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
</details>
运行结果:
那展开收起那部分的内容只能放在<summary>
标签之后吗?如果放它前面呢:
<details>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
<summary>公众号:</summary>
</details>
运行结果:
效果居然一模一样,看来展开收起的那部分应该是在<details>
标签内部的除<summary>
标签之外的所有内容。那如果写两个<summary>
标签呢:
<details>
<button>前端学不动</button>
<span>前端学不动</span>
<h1>前端学不动</h1>
<a href="#">前端学不动</a>
<strong>前端学不动</strong>
<summary>公众号:</summary>
<summary>summary</summary>
</details>
运行结果:
可以看到只有第一个出现的<summary>
标签是真正的summary
,后续出现的其他所有标签(包括其它的<summary>)都是展开收起的那部分。
既然所有标签都可以,那么也包括<details>
咯?
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
运行结果:
这玩意有点意思,利用这种嵌套写法可以轻松实现编辑器左侧的那些文件区的效果。
加入样式
虽然可以很轻松、甚至在不用写 CSS 代码的情况下就实现展开收起效果,但毕竟不写 CSS 只是实现了个最基础的乞丐版效果,很多人都不想要点击的时候出现的那个轮廓:
在谷歌浏览器和 Safari 浏览器下都会出现这个轮廓,火狐就没有这玩意,咱们只需要给<summary>
标签设置 outline 属性就可以了,一般如果你的项目引入了抹平浏览器样式间差异的 reset.css 文件的话,就不用写这个 CSS 了,为了方便同时观看 HTML、CSS 和 JS,我们来用 Vue 的格式来写代码:
<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>
<style>
summary { outline: none }
</style>
运行结果:
这样看起来就舒服多啦!但是还有个问题:那个三角箭头太傻大黑粗了,一般我们很少会用这样的箭头,而且我们也不一定非得让它在左边待着,那么怎么修改箭头的样式呢?
在谷歌浏览器以及 Safari 浏览器下我们需要用::-webkit-details-marker
伪元素,在火狐浏览器下我们要用::-moz-list-bullet
伪元素,比如我们想让它别那么傻大黑粗:
<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>
<style>
summary { outline: none }
/* 谷歌、Safari */
::-webkit-details-marker {
transform: scale(.5);
color: gray
}
/* 火狐 */
::-moz-list-bullet { color: gray }
</style>
运行结果:
是不是没那么傻大黑粗了,不过有时我们不想要这个三角形的箭头,想要的是自己自定义的箭头,那么我们就需要先把这个默认的三角给隐藏掉:
<template>
<details>
<summary>project</summary>
<details>
<summary>html</summary>
index.html
</details>
<details>
<summary>css</summary>
reset.css
</details>
<details>
<summary>js</summary>
main.js
</details>
</details>
</template>
<style>
summary { outline: none }
/* 谷歌、Safari */
::-webkit-details-marker { display: none }
/* 火狐 */
::-moz-list-bullet { font-size: 0 }
</style>
运行结果:
这回箭头没了,我们只需要在<summary>
标签里写个箭头就好了,可以用::before
或::after
伪元素,也可以直接在里面写个<img>
标签,为了让大家能够直接复制代码到 Vue 环境里运行,在这里我们就不用图片了,直接手写<svg>
:
<template>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
project
</summary>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
html
</summary>
index.html
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
css
</summary>
reset.css
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
js
</summary>
main.js
</details>
</details>
</template>
<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}
/* 谷歌、Safari */
::-webkit-details-marker { display: none }
/* 火狐 */
::-moz-list-bullet { font-size: 0 }
svg {
position: absolute;
left: 0;
top: 50%;
fill: none;
stroke: gray
}
</style>
运行结果:
箭头是变成自定义的了,但是方向却不智能了,不能像原生箭头那样展开收起时会自动改变方向,但是<details>
这个标签好就好在它在展开是会自动在标签里添加一个open
属性:
我们可以利用它的这一特点,用属性选择器来让<svg>
标签进行旋转:
<template>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
project
</summary>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
html
</summary>
index.html
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
css
</summary>
reset.css
</details>
<details>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
js
</summary>
main.js
</details>
</details>
</template>
<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}
/* 谷歌、Safari */
::-webkit-details-marker { display: none }
/* 火狐 */
::-moz-list-bullet { font-size: 0 }
svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}
[open] > summary > svg { transform: none }
</style>
运行结果:
用 JS 控制 open 属性
既然展开时会自动给<details>
标签添加一个open
属性,那如果我们用 JS 手动给<details>
标签添加或删除open
属性,<details>
标签会随之展开收起吗?
比如我们用定时器,每隔1秒就自动展开一个,同时收起上一个已被展开过的标签:
<template>
<details v-for="({title, content}, index) of list" :key="title" :open="openIndex === index">
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
{{ content }}
</details>
</template>
<script>
import { defineComponent, ref, onBeforeUnmount } from 'vue'
export default defineComponent(() => {
const list = [{
title: 'html',
content: 'index.html'
}, {
title: 'css',
content: 'reset.css'
}, {
title: 'js',
content: 'main.js'
}]
const openIndex = ref(-1)
const interval = setInterval(() => openIndex.value === list.length
? openIndex.value = 0
: openIndex.value++
, 1000)
onBeforeUnmount(() => clearInterval(interval))
return { list, openIndex }
})
</script>
<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}
/* 谷歌、Safari */
::-webkit-details-marker { display: none }
/* 火狐 */
::-moz-list-bullet { font-size: 0 }
svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}
[open] > summary > svg { transform: none }
</style>
运行结果:
既然能靠控制open
属性来控制元素的展开收起,那么手风琴效果也很好实现了:只需要保证在当前列表中仅有一个<details>
标签有open
属性,点击别的标签时就去掉另一个标签的open
属性即可:
<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
{{ content }}
</details>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent(() => {
const list = [{
title: 'html',
content: 'index.html'
}, {
title: 'css',
content: 'reset.css'
}, {
title: 'js',
content: 'main.js'
}]
const openIndex = ref(-1)
const onChange = ({ target }, i) => target.open && (openIndex.value = i)
return { list, openIndex, onChange }
})
</script>
<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}
/* 谷歌、Safari */
::-webkit-details-marker { display: none }
/* 火狐 */
::-moz-list-bullet { font-size: 0 }
svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}
[open] > summary > svg { transform: none }
</style>
运行结果:
加入动画
那么接下来离一个理想的手风琴效果只差最后一步了:过渡动画
但过渡动画这里有坑,我们先来分析一下思路:在平时就给<details>
标签里的内容区(除第一个出现的
然后在 open 时用属性选择器
[open]
配合后代选择器来给内容区加上 max-height: xxx; 的代码,这样平时在收起时高度就是0,等出现 open 属性时就会慢慢过渡到我们定义的最大高度:
<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</details>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]
const openIndex = ref(-1)
const onChange = ({ target }, i) => target.open && (openIndex.value = i)
return { list, openIndex, onChange }
})
</script>
<style>
summary {
position: relative;
padding-left: 20px;
outline: none
}
/* 谷歌、Safari */
::-webkit-details-marker { display: none }
/* 火狐 */
::-moz-list-bullet { font-size: 0 }
svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}
details > ul {
max-height: 0;
margin: 0;
overflow: hidden;
}
[open] > summary > svg { transform: none }
[open] > ul { max-height: 120px }
</style>
运行结果:
如果用谷歌浏览器打开的话居然看不到任何的过渡效果!但用火狐打开就有效果:
估计是浏览器的 bug,既然过渡动画(transition)在不同浏览器之间表现不一致,那关键帧动画(keyframes)呢?
<template>
<details
v-for="({title, content}, index) of list"
:key="title"
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</details>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]
const openIndex = ref(-1)
const onChange = ({ target }, i) => target.open && (openIndex.value = i)
return { list, openIndex, onChange }
})
</script>
<style lang="scss">
summary {
position: relative;
padding-left: 20px;
outline: none
}
/* 谷歌、Safari */
::-webkit-details-marker { display: none }
/* 火狐 */
::-moz-list-bullet { font-size: 0 }
svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}
details > ul {
max-height: 0;
margin: 0;
overflow: hidden;
}
[open] {
> summary > svg { transform: none }
> ul { animation: open .2s both }
}
@keyframes open {
to { max-height: 120px }
}
</style>
运行结果:
可以看到关键帧动画在各大浏览器的行为都是一致的,推荐大家使用关键帧动画。
收起动画
上面那种效果已经完全足够满足我们的日常开发需求了,但它仍然有一个小小的遗憾,那就是:收起的时候没有任何的动画效果。
那么怎么才能解决这个问题呢?答案就是更改 DOM 结构,我们把原本放在<details>
里面那部分需要展开收起的内容元素移到<details>
标签的外面去,但一定要在它的后一位,这样就可以方便我们用兄弟选择器配合属性选择器来控制外部元素的显隐了,在<details>
标签有 open 属性时我们就让它的后面一个元素用动画展开,没有 open 属性时我们就让后一个元素用动画收起:
<template>
<template v-for="({title, content}, index) of list" :key="title">
<details
:open="openIndex === index"
@toggle="onChange($event, index)"
>
<summary>
<svg width="16" height="7">
<polyline points="0,0 8,7 16,0"/>
</svg>
{{ title }}
</summary>
</details>
<ul>
<li v-for="doc of content" :key="doc">{{ doc }}</li>
</ul>
</template>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent(() => {
const list = [{
title: 'html',
content: ['index.html', 'banner.html', 'login.html', '404.html']
}, {
title: 'css',
content: ['reset.css', 'header.css', 'banner.css', 'footer.css']
}, {
title: 'js',
content: ['index.js', 'main.js', 'javascript.js']
}]
const openIndex = ref(-1)
const onChange = ({ target }, i) => target.open && (openIndex.value = i)
return { list, openIndex, onChange }
})
</script>
<style lang="scss">
summary {
position: relative;
padding-left: 20px;
outline: none
}
/* 谷歌、Safari */
::-webkit-details-marker { display: none }
/* 火狐 */
::-moz-list-bullet { font-size: 0 }
svg {
position: absolute;
left: 0;
top: 50%;
transform: rotate(180deg);
transition: transform .2s;
fill: none;
stroke: gray
}
ul {
max-height: 0;
margin: 0;
transition: max-height .2s;
overflow: hidden
}
[open] {
> summary > svg { transform: none }
+ ul { max-height: 120px }
}
</style>
运行结果:
结语
如果你的项目不需要这些花里胡哨的动画效果,完全可以只靠 H5 标签去实现,根本不必再去关心展开收起的逻辑了,只需要写一些样式代码就可以了,比如写成暗黑模式:
你的 CSS 只需要专注于暗黑模式本身就够了,是不是很省心呢?
同时这个收拉效果也并不仅仅只适用于手风琴,很多地方都可以用到它,比如这种:
但唯一比较遗憾的事就是这个标签不支持 IE:
不过好在别的浏览器支持的都不错,如果你的项目不需要兼容 IE 的话就请尽情的享受<details>
标签所带来的便利吧!
本文首发于公众号:《前端学不动》
往期精彩文章
- 《整治GitHub不文明现象!微软推出评论区!》
- 《Vue 3.0.3 : 新增CSS变量传递以及最新的Ref提案》
- 《[译]尤雨溪: Ref语法糖提案》
- 《双11小黑盒很炫酷?咱们用CSS变量来改进一下!》
- 《千万别小瞧九宫格 一道题就能让候选人原形毕露!》
- 《移动端布局面试题 全面考察你的CSS功底(居中篇)》
- 《将原型对象设置成Proxy后的一系列迷惑行为》
- 《Vue超好玩的新特性:DOM传送门》
- 《不依赖任何库打造属于自己的可视化数据地图》
- 《在Vue项目中使用React超火的CSS-in-JS库: styled-components》
- 《终于轮到Vue来带给React灵感了?》
- 《Vue3在IOS下的一个小坑》
- 《2020要用immer来代替immutable优化你的React项目》
- 《来自《React Hooks 与 Immutable》小册作者'神三元'的灵魂拷问》
- 《好消息,Vue3官方文档出中文版的啦!》
- 《新版vue-router的hooks用法》
- 《[译]Vue 3:2020年中状态更新》
- 《[译]React 17终于发布RC版本了 官方竟说17是个过渡版!》
- 《[译]尤雨溪:Vue3的设计过程》
- 《Node之父重构的Deno终于发布了,它终究会取代Node吗?》
- 《今日凌晨Vue3 beta版震撼发布,竟然公开支持脚手架项目!》
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!