乐趣区

关于前端:仅靠H5标签就能实现收拉效果我说的是真的

前言

最近做我的项目时碰到这么一个需要:


这有点相似于手风琴成果,但不一样的是很多手风琴成果是同一时间内只能有一个开展,而这个是各个局部独立的,你展不开展齐全不会影响我的开展与否。其实这种成果几乎再广泛不过了,网上轻易一搜就进去一大堆。但不一样的是,我在接到这个需要的时候忽然想起来很久以前看过张鑫旭大佬的一篇文章,含糊的记得那篇文章里说过有个什么很不便的 CSS 属性可能实现这一成果,不必像咱们平时实现的那些开展收起那样写很多的代码,于是就来到他的博客外面一顿搜,找了半天终于发现原来是我记错了,并不是什么 CSS3 属性,而是 HTML5 标签!

details

想要十分轻松的实现一个收拉成果,须要用到三个标签,别离是:<details><summary>以及 随便

随便是什么意思?意思是什么标签都能够?

咱们先只写一个 <details> 标签来看看页面上会呈现什么:

<details></details>

运行后果:


能够看到十分有意思的一个景象:咱们明明什么文字都没有写,但页面上却呈现了 详细信息 这四个字,因为如果你在标签里没有写 <summary> 的话,浏览器会主动给你补上一个 <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> 标签开展收起时会触发一个 toggle 事件,和 click、mousemove 等事件用法统一,也会接管一个 event 对象的参数,event.target 是以后触发事件的 DOM,也就是 <details>,它会有一个.open 属性,值为 true 或 false,代表是否开展收起。

退出动画

那么接下来离一个现实的手风琴成果只差最初一步了:过渡动画

但过渡动画这里有坑,咱们先来剖析一下思路:在平时就给 <details> 标签里的内容区 ( 除第一个呈现的 <summary> 标签以外的内容 ) 写上:max-height: 0;
而后在 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>

运行后果:

能够看到关键帧动画在各大浏览器的行为都是统一的,举荐大家应用关键帧动画。

收起动画

下面那种成果曾经齐全足够满足咱们的日常开发需要了,但它依然有一个小小的遗憾,那就是:收起的时候没有任何的动画成果。

这是因为 <details> 的行为是靠着 open 属性管制内容显示或暗藏,你能够简略的把它的暗藏了解为 display: block;display: none;,尽管这么说可能并不精确,但却十分有助于咱们了解 <details> 的行为:在开展时 display: block; 忽然显示,既然显示了就能够有工夫展现咱们的开展动画。但在收起时 display: none; 是忽然隐没,基本没工夫展现咱们的收起动画。

那么怎么能力解决这个问题呢?答案就是更改 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> 标签所带来的便当吧!

本文首发于公众号:《前端学不动》

退出移动版