首页 > 其他分享 >记录--多行标签超出展开折叠功能

记录--多行标签超出展开折叠功能

时间:2023-07-03 18:00:51浏览次数:51  
标签:多行 容器 const -- 标签 getBoundingClientRect labels listCon

这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助

前言

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

出处

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

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

功能实现

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

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

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

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

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

具体实现上代码:

<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仓库。

本文转载于:

https://juejin.cn/post/7251394142683742269

如果对您有所帮助,欢迎您点个关注,我会定时更新技术文档,大家一起讨论学习,一起进步。

 

标签:多行,容器,const,--,标签,getBoundingClientRect,labels,listCon
From: https://www.cnblogs.com/smileZAZ/p/17523562.html

相关文章

  • Typora实现Markdown标题自动编号
    1、背景Typora编写Markdown时,各级标题需要手动维护编号,如果标题顺序有调整,需要依次手工重新修改编号,特别是多级标题都要调整的话,更是异常麻烦!昨天在网上看到一个通过修改Typora风格主题的css文件实现自动编号的方法,试用之后感觉非常nice,再也不用管编号了,简直不要太爽!2、原文在此......
  • Silhouette 2023.0.1 CE 影视后期ROTO跟踪抠像合成软件 支持AE/PR/达芬奇/VEGAS/OFX插
    Silhouette是一款被广泛应用于影视剧中Roto、抠像、擦威亚的特效合成辅助软件,正所谓术业有专攻,它就是为了应对这些脏活累活而诞生的。之前还有一款软件CommotionPro,但是已经停止开发,目前已经被这款Silhouette所替代,目前它也属于BorisFX家族的一员。软件下载Silhouette2023.......
  • word文档的图片怎么保存到KindEditor上
    ​ 这种方法是servlet,编写好在web.xml里配置servlet-class和servlet-mapping即可使用后台(服务端)java服务代码:(上传至ROOT/lqxcPics文件夹下)<%@ page language="java" import="java.util.*" pageEncoding="utf-8"%><%@     page contentType="text/html;cha......
  • META-INF/services 目录有什么作用
    META-INF/services目录是Java标准的服务提供者配置目录,用于在应用程序中声明和发现服务提供者的实现类。具体而言,它的作用有以下几点:服务发现:该目录下的文件用于标识服务接口,并声明服务的具体实现类。这些文件的命名以服务接口的全限定名为准,文件内容为实现类的全限定名。......
  • Unity Visual Studio 常用的自带配置以及快捷键
    ​完整文档->UnityVisualStudio常用的自带配置以及快捷键一、内联提示 不等号字体 快速创建新文件快捷键:Ctrl+shift+A 选择下一项与多行光标Alt+左键功能增强需安装插件Select Next Occurrence 拆分窗口窗口右上角 折叠大纲​......
  • 简单的python面向对象案例——跑步或吃饭
    个人学习,仅供参考要求对象:小明a.属性:姓名,体重b.方法:跑步,吃东西(每次跑步会减掉0.1kg,每次吃东西增加0.2kg)输入名字以初始体重选择跑步或吃东西,输入次数打印当前体重代码如下:#定义一个类classPerson(object):#公共属性def__init__......
  • 在Jupyter笔记本中使用Python与GPT-4进行交互
    在这篇文章中,我们将讨论如何在Jupyter笔记本中使用Python与GPT-4(一种强大的自然语言处理模型)结合进行处理。尽管OpenAI并未特地发布名为"GPT-4"的模型,但我们可以使用现有的GPT-3作为参考。如OpenAI未来发布了GPT-4,其与GPT-3的用法将会非常相似。在Jupyter笔记本中使用Python与GPT......
  • 使用maxent来研究害虫的适生性分析
    源程序下载https://biodiversityinformatics.amnh.org/open_source/maxent/ 源代码https://github.com/mrmaxent/Maxent 使用手册https://clp-foss4g-workshop.readthedocs.io/en/latest/maxent_install.html 文献引用StevenJ.Phillips,RobertP.Anderson,Rober......
  • 基于差速驱动移动基座的三维变型机器人轨迹优化
    在执行任务时,服务机器人的功能结构变化可能会限制其自主导航能力,从而影响其行动力。本文的研究,旨在解决复杂三维环境中可变形机器人的轨迹规划问题,特别是应用最为广泛的基于差速驱动移动基座的移动机器人的轨迹规划。这种全局轨迹优化方法是将机器人整个身体的轨迹建模为一个多项......
  • Genuine Intel(R) CPU型号
    起因:在盘点固定资产的时候,发现有一台电脑CPU不显示具体型号,而是 英特尔@2.60GHz(X2),通过主板型号来判断是至强系列的CPU,后经软件识别为Genuine,然后去查资料才了解到该CPU的相关信息 GenuineIntel(R)CPU GenuineIntel(R)CPU是INTEL正式版CPU的意思,INTELCPU有3......