多行标签展开隐藏功能

前言

 记录分享每一个日常开发项目中的实用小知识,不整那些虚头巴脑的框架理论与原理,之前分享过抽奖功能、签字功能等,有兴趣的可以看看本人以前的分享。
 今天要分享的实用小知识是最近项目中遇到的标签相关的功能,我不知道叫啥,姑且称之为【多行标签展开隐藏】功能吧,类似于多行文本展开折叠功能,如果超过最大行数则显示展开隐藏按钮,如果不超过则不显示按钮。多行文本展开与折叠功能在网上有相当多的文章了,也有许多开源的封装组件,而多行标签展开隐藏的文章却比较少,刚好最近我也遇到了这个功能,所以就单独拿出来与大家分享如何实现。

出处

 【多行标签展开与隐藏】该功能我们平时可能没注意一般在哪里会有,其实最常见的就是各种APP的搜索页面的历史记录这里,下面是我从拼多多(左)和腾讯学堂小程序(右)截下来的功能样式:

多行标签案列图(pdd/txxt)

其它APP一般搜索的历史记录这里都有这个小功能,比如京东、支付宝、淘宝、抖音、快手等,可能稍有点儿不一样,有的是按钮样式,有的是只有展开没有收起功能,可能我们用过了很多年平时都没有注意到这个小功能,有想了解的可以去看一看哈。如果有一天你们需要开发一个搜索页面的话产品就很有可能出这样的一个功能,接下来我们就来看看这种功能我们该如何实现。

功能实现

我们先看实现的效果图,然后再分析如何实现

GIF 2023-6-30 效果图.gif

【样式一】:标签容器和展开隐藏按钮分开(效果图样式一)

 标签容器和按钮分开的这种样式功能实现起来的话我个人觉得难度稍微简单一些,下面我们看看如何实现这种分开的功能。

第一种方法:通过与第一个标签左偏移值对比实现

原理:遍历每个标签然后通过与第一个标签左偏移值对比,如果有几个相同偏移值则说明有几个换行

具体实现上代码:

<div class="list-con list-con-1">
    <div class="label">人工智能</div>
    <div class="label">人工智能与应用</div>
    <div class="label">行业分析与市场数据</div>
    <div class="label">标签标签标签标签标签标签标签标签</div>
    <div class="label">标签</div>
    <div class="label">啊啊啊</div>
    <div class="label">宝宝贝贝</div>
    <div class="label">微信</div>
    <div class="label">吧啊啊</div>
    <div class="label">哦哦哦哦哦哦哦哦</div>
</div>
<div class="expand expand-1">展开 ∨</div>
<script>
  const listCon = document.querySelector('.list-con-1')
  const expandBtn = document.querySelector('.expand-1')
  console.log(listCon.children)
  // HTMLCollection对象 item()、namedItem()方法 length属性
  let firstLabelOffsetLeft = 0 // 第一个标签左侧偏移
  let line = 1 // 记录行
  const len = listCon.children.length
  for(let i = 0; i < len; i++) {
    const _offsetLeft = listCon.children.item(i).offsetLeft
    if (i === 0) {
      firstLabelOffsetLeft = _offsetLeft
    } else if (firstLabelOffsetLeft === _offsetLeft) {
      line++
      console.log(line + '行')
    }
  }
  // 如果大于一行则隐藏
  if (line > 2) {
    expandBtn.style = 'display: show'
  } else {
    expandBtn.style = 'display: none'
  }
  expandBtn.addEventListener('click', () => {
    const _classList = listCon.classList
    if (_classList.contains('list-expand')) {
      expandBtn.innerHTML = '展开 ∨'
    } else {
      expandBtn.innerHTML = '收起 ∧'
    }
    _classList.toggle('list-expand') // 这个更简洁
  })

</script>

解析:HTML布局就不用多说了,是个前端都知道该怎么搞,如果不知道趁早送外卖去吧,多说无益,把机会留给其他人。其次CSS应该也是比较简单的,注意的是有个前提需要先规定容器的最大高度,然后使用overflow超出隐藏,这样展开就直接去掉该属性,让标签自己撑开即可。JavaScript部分我这里没有使用啥框架,因为这块实现就是个简单的Demo所以就用纯原生写比较方便,这里我们先获取容器,然后获取容器的孩子节点(这里我们也可以直接通过className查询出所有标签元素),返回的是一个可遍历的变签对象,然后我们记录第一个标签的offsetLeft左偏移值,接下来遍历所有的标签元素,如果有与第一个标签相同的值则累加,最终line表示有几行,如果超过我们最大行数(demo超出2行隐藏)则显示展开隐藏按钮。

第二种方法:通过计算容器高度对比

原理:通过容器底部与标签top比较,如果有top值大于容器底部bottom则表示超出容器隐藏。

具体上代码:

<script>
  const listCon2 = document.querySelector('.list-con-2')
  const expandBtn2 = document.querySelector('.expand-2')
  const listCon2Height = listCon2.getBoundingClientRect().bottom
  const len2 = listCon2.children.length
  for(let i = 0; i < len2; i++) {
    const _top = listCon2.children.item(i).getBoundingClientRect().top
    // 通过top判断如果有标签大于容器bottom则隐藏
    if (_top >= listCon2Height) {
      expandBtn2.style = 'display: show'
      break
    } else {
      expandBtn2.style = 'display: none'
    }
  }
  expandBtn2.addEventListener('click', () => {
    const _classList = listCon2.classList
    // console.log(_classList)
    if (_classList.contains('list-expand')) {
      expandBtn2.innerHTML = '展开 ∨'
    } else {
      expandBtn2.innerHTML = '收起 ∧'
    }
    _classList.toggle('list-expand')
  })
</script>

解析:HTMLCSS同方法一同,不同点在于这里是通过getBoundingClientRect()方法来判断,还是遍历所有标签,不同的是如果有标签的top值大于等于了容器的bottom值,则说明了标签已超出容器,则要显示展开隐藏按钮,展开隐藏还是通过容器overflow属性来实现比较简单。

【样式二】:展开隐藏按钮和标签同级(效果图样式二)

 这种样式也是绝大部分APP产品使用的风格,不信你可以打开抖音商城或汽车之家的搜索历史,十个产品九个是这样设计的,不是这样的我倒立洗头。
 这种放在同级的就相对稍微难一点,因为要把展开隐藏按钮塞到标签的最后,如果是隐藏的话就要切割标签展示数量,那下面我就带大家看看我是是如何实现的。

方法一:通过遍历高度判断

原理:同样式一的高度判断一样,通过容器底部bottom与标签top比较,如果有top值大于容器顶部bottom则表示超出容器隐藏,不同的是如何计算标签展示的长度。有个前提是按钮和标签的的宽度要做限制,最好是一行能放一个标签和按钮。

具体实现上代码:

<div id="app3">
  <div class="list-con list-con-3" :class="{'list-expand': isExpand}">
    <div class="label" v-for="item in labelArr.slice(0, labelLength)">{{ item }}</div>
    <div class="label expand-btn" v-if="showExpandBtn" @click="changeExpand">{{ !isExpand ? '展开 ▼' : '隐藏 ▲' }}</div>
  </div>
</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
  const { createApp, nextTick } = Vue
  createApp({
    props: {
      maxLine: {
        type: Number,
        default: 2
      }
    },
    data () {
      return {
        labelArr: [],
        isExpand: false,
        showExpandBtn: false,
        labelLength: 0,
        hideLength: 0
      }
    },
    mounted () {
      const labels = ['人工智能', '人工智能与应用', '行业分析与市场数据', '标签标签标签标签标签标签标签', '标签A', '啊啊啊', '宝宝贝贝', '微信', '吧啊啊', '哦哦哦哦哦哦哦哦', '人工智能', '人工智能与应用']
      
      this.labelArr = labels
      this.labelLength = labels.length
      nextTick(() => {
        this.init()
      })
    },
    methods: {
      init () {
        const listCon = document.querySelector('.list-con-3')
        const labels = listCon.querySelectorAll('.label:not(.expand-btn)')
        const expandBtn = listCon.querySelector('.expand-btn')

        let labelIndex = 0 // 渲染到第几个
        const listConBottom = listCon.getBoundingClientRect().bottom // 容器底部距视口顶部距离
        for(let i = 0; i < labels.length; i++) {
          const _top = labels[i].getBoundingClientRect().top
          if (_top >= listConBottom ) { // 如果有标签顶部距离超过容器底部则表示超出容器隐藏
            this.showExpandBtn = true
            console.log('第几个索引标签停止', i)
            labelIndex = i
            break
          } else {
            this.showExpandBtn = false
          }
        }
        if (!this.showExpandBtn) {
          return
        }
        nextTick(() => {
          const listConRect = listCon.getBoundingClientRect()
          const expandBtn = listCon.querySelector('.expand-btn')
          const expandBtnWidth = expandBtn.getBoundingClientRect().width
          const labelMaringRight = parseInt(window.getComputedStyle(labels[0]).marginRight)
          for (let i = labelIndex -1; i >= 0; i--) {
            const labelRight = labels[i].getBoundingClientRect().right - listConRect.left
            if (labelRight + labelMaringRight + expandBtnWidth <= listConRect.width) {
              this.hideLength = i + 1
              this.labelLength = this.hideLength
              break
            }
          }    
        })
      },
      changeExpand () {
        this.isExpand = !this.isExpand
        console.log(this.labelLength)
        if (this.isExpand) {
          this.labelLength = this.labelArr.length
        } else {
          this.labelLength = this.hideLength
        }
      }
    }
  }).mount('#app3')
</script>

解析:同级样式Demo我们使用vue来实现,HTML布局和CSS样式没有啥可说的,还是那就话,不行真就送外卖去比较合适,这里我们主要分析一下Javascript部分,还是先通过getBoundingClientRect()方法来获取容器的bottom和标签的top,通过遍历每个标签来对比是否超出容器,然后我们拿到第一个超出容器的标签序号,就是我们要截断的长度,这里是通过数组的slice()方法来截取标签长度,接下来最关建的如何把按钮拼接上去,因为标签的宽度是不定的,我们要把按钮显示在最后,我们并不确定按钮拼接到最后是不是会导致宽度不够超出,所以我们倒叙遍历标签,如果(最后一个标签的右边到容器的距离right值+标签的margin值+按钮的width)和小于容器宽度,则说明展示隐藏按钮可以直接拼接在后面,否则标签数组长度就要再减一位来判断是否满足。然后展开隐藏功能就通过切换原标签长度和截取的标签长度来完成即可。

方法二:通过与第一个标签左偏移值对比实现

原理:同样式一的方法原理,遍历每个标签然后通过与第一个标签左偏移值对比判断是否超出行数,然后长度截取同方法一一致。

直接上代码:

<script>
  const { createApp, nextTick } = Vue
  createApp({
    data () {
      return {
        labelList: [],
        isExpand: false,
        showExpandBtn: false,
        labelLength: 0,
        hideLength: 0
      }
    },
    mounted () {
      const labels = ['人工智能', '人工智能与应用', '行业分析与市场数据报告行业分析与市场数据报告', '标签标签标签标签标签标签标签', '标签A', '啊啊啊', '宝宝贝贝', '微信', '吧啊啊', '哦哦哦哦哦哦哦哦', '人工智能', '人工智能与应用']
      this.labelList = labels
      this.labelLength = labels.length
      
      nextTick(() => {
        this.init()
      })
      
    },
    methods: {
      init () {
        const listCon = document.querySelector('.list-con-4')
        const labels = listCon.querySelectorAll('.label:not(.expand-btn)')
        const firstLabelOffsetLeft = labels[0].getBoundingClientRect().left // 第一个标签左侧偏移量
        const labelMaringRight = parseInt(window.getComputedStyle(labels[0]).marginRight)
        let line = 0 // 几行
        let labelIndex = 0 // 渲染第几个
        for(let i = 0; i < labels.length; i++) {
          const _offsetLeft = labels[i].getBoundingClientRect().left
          if (firstLabelOffsetLeft === _offsetLeft) {
            line += 1
          }
          console.log(line, i)
          if (line > 2) {
            this.showExpandBtn = true
            labelIndex = i
            // this.labelLength = this.hideLength
            break
          } else {
            this.showExpandBtn = false
          }
        }
        if (!this.showExpandBtn) {
          return
        }
        nextTick(() => {
          const listConRect = listCon.getBoundingClientRect()
          const expandBtn = listCon.querySelector('.expand-btn')
          console.log(listConRect, expandBtn.getBoundingClientRect())
          const expandBtnWidth = expandBtn.getBoundingClientRect().width
          for (let i = labelIndex -1; i >= 0; i--) {
            console.log(labels[i])
            const labelRight = labels[i].getBoundingClientRect().right - listConRect.left
            console.log(labelRight, expandBtnWidth, labelMaringRight)
            if (labelRight + labelMaringRight + expandBtnWidth <= listConRect.width) {
              this.hideLength = i + 1
              this.labelLength = this.hideLength
              break
            }
          }    
        })
      },
      changeExpand () {
        this.isExpand = !this.isExpand
        if (this.isExpand) {
          this.labelLength = this.labelList.length
        } else {
          this.labelLength = this.hideLength
        }
      }
    }
  }).mount('#app4')
</script>

这里也无需多做解释了,直接看代码即可。

结尾

上面就是【多行标签展开隐藏】功能的基本实现原理,网上相关实现比较少,我也是只用了Javascript来实现,如果可以纯靠CSS实现,有更简单或更好的方法实现可以留言相互交流学。代码没有封装成组件,但是具有一些参考意义,用于生产可以自己去封装成组件使用,完整的代码在我的GitHub仓库


本文是笔者总结编撰,如有偏颇,欢迎留言指正,若您觉得本文对你有用,不妨点个赞~

关于作者:
GitHub
简书
掘金

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 225,928评论 6 523
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 97,032评论 3 410
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 173,382评论 0 370
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 61,580评论 1 304
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 70,558评论 6 403
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 54,018评论 1 316
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 42,261评论 3 432
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 41,328评论 0 281
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 47,858评论 1 328
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 39,843评论 3 351
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 41,954评论 1 358
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 37,565评论 5 352
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 43,251评论 3 342
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 33,677评论 0 25
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 34,834评论 1 278
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 50,558评论 3 383
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 47,033评论 2 368

推荐阅读更多精彩内容