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

news2024/9/20 23:55:24

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

前言

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

出处

 【多行标签展开与隐藏】该功能我们平时可能没注意一般在哪里会有,其实最常见的就是各种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

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

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/715056.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Squid代理服务器(传统代理、透明代理)

Squid代理服务器&#xff08;传统代理、透明代理&#xff09; 一、Squid代理服务器1、代理的工作机制2、代理的类型3、使用代理的好处 二、安装Squid服务2、修改 Squid 的配置文件3、Squid 的运行控制4、编写 Squid 服务脚本 三、构建传统代理服务器1、squid服务器2、web1的配置…

libevent实践02:读取标准输入的数据

对于libevent的知识一点不了解&#xff0c;通知编写测试代码&#xff0c;发现问题&#xff0c;解决问题&#xff0c;一步一步入门学习。 CMakeLists.txt文件&#xff1a; project(libevent_project) cmake_minimum_required(VERSION 3.8)message(STATUS "lkmao:CMAKE_SOU…

Redis高可用群集---搭建(主从、哨兵、Cluster)

目录 Redis 高可用集群Redis 主从复制Redis 哨兵模式Redis 集群模式 Redis 高可用集群 在web服务器中&#xff0c;高可用是指服务器可以正常访问的时间&#xff0c;衡量的标准是在多长时间内可以提供正常服务&#xff08;99.9%、99.99%、99.999%等等&#xff09;。 但是在Redi…

# YourKit Java Profiler 教程

YourKit Java Profiler 教程 文章目录 YourKit Java Profiler 教程本教程软件版本说明YourKitIntelliJ Idea 系统要求探查器体系结构探查器代理探查器用户界面 开始分析集成环境集成IntelliJ Idea 安装插件IntelliJ Idea分析 解决性能问题性能瓶颈优化内存使用内存泄漏垃圾分配…

数据结构 | 双向链表

一、数据结构定义 /* 链表结点 */ typedef int ListType; typedef struct node {ListType data; // 存放整型数据struct node* llink, * rlink; // 指向前驱和后继结点的指针 } *Node;/* 双向链表头结点 */ typedef struct headNode {struct node* head, * tail; // 指向双向链…

IP 扫描程序:轻松发现、扫描和跟踪 IP 空间

什么是 IP 扫描 IP 扫描是实时持续监控网络 IP 地址空间的过程。包括 ICMP ping 扫描和 SNMP 扫描在内的网络协议数量用于扫描网络中的 IP 地址。网络管理员依靠 IP 扫描程序轻松检查和管理 IP 地址空间。使用网络 IP 扫描程序进行 IP 扫描可查看 IP 地址空间利用率和性能。 …

C# 使用CefSharp控件 High DPI问题的解决

使用CefSharp控件&#xff0c;在部分高分辨率的电脑中&#xff08;显示缩放比例非100%&#xff0c;而是120%或者125%等&#xff09;会出现以下一些情况&#xff1a; 显示的页面中出现了黑边&#xff0c;且按钮定位也偏了&#xff0c;比如点击【图层】按钮&#xff0c;需要点击上…

selenium用法

一、请求库selenium selenium是一个自动化测试工具&#xff0c;而爬虫中使用它主要是为了解决requests无法直接执行JavaScript代码的问题。操作浏览器模拟人的行为。 下载浏览器驱动&#xff1a;以谷歌浏览器为例---->谷歌浏览器驱动&#xff08;版本号对应&#xff09; …

K8S应用生命周期管理

K8S应用生命周期管理. 1 应用周期管理1.1 资源对象1.1.1 基础知识1.1.2 资源属性 1.2 Pod基础1.2.1 Pod概述1.2.2 简单实践1.2.3 流程解读1.2.4 应用解析1.2.5 初始化容器1.2.6 Sidecar实践1.2.7 静态POD实践 1.3 Pod进阶1.3.1 Pod探测机制1.3.2 命令探测1.3.3 TCP探测1.3.4 HT…

C# +.Net医院检验科LIS系统源码 实验室信息系统源码

实验室信息系统&#xff08;Laboratory Information System&#xff0c;缩写LIS&#xff09;是一类用来处理实验室过程信息的软件。这套系统通常与其他信息系统比如医院信息系统&#xff08;HIS&#xff09;连接。实验室信息系统由多种实验室流程模块构成&#xff0c;这些模块可…

79-基于stm32单片机酒精浓度测量疲劳驾驶检测系统(程序+原理图+元件清单全套资料)...

资料编号&#xff1a;079 功能介绍&#xff1a;采用stm32单片机作为主控CPU&#xff0c;采用MQ3传感器采集酒精浓度&#xff0c;采用红外接触传感器感应驾驶员上车时间&#xff0c;OLED显示酒精浓度和驾驶时间&#xff0c;当酒精浓度超过阈值&#xff08;程序可调&#xff09;&…

【QT】——多线程的使用

目录 基本概念 1.线程类QThread 1.1信号和槽 1.2静态函数 1.3 任务处理函数 2.实例 第一种方式 第二种方式 基本概念 默认的线程在Qt中称之为窗口线程&#xff0c;也叫主线程&#xff0c;负责窗口事件处理或者窗口控件数据的更新子线程负责后台的业务逻辑处理&#xff…

Rocky Linux能否通过其他方法合法地获得RHEL源代码?让我们一起来看看吧

在红帽公司限制对RHEL源代码的访问后&#xff0c;Rocky Linux寻找了替代方案来确保他们可以继续获取所需的源代码并行使他们的权利。他们认为这种限制违反了开源的精神和目的&#xff0c;因此积极寻求解决方案&#xff0c;以维护他们对开源软件的承诺。那么Rocky Linux能否通过…

AOM、VTM初体验及安装tensorflow

AOM、VTM初体验 Cmake cmake的定义是什么 &#xff1f;-----高级编译配置工具 当你用不同的语言或者编译器开发一个项目&#xff0c;各就各位code完之后要生成最终的输出&#xff08;dll 或执行文件&#xff09;&#xff0c;这时候就尴尬了&#xff0c;你要手动去MingGW或者…

Kotlin 1.9 新特性预览:data object (数据单例)

前言 data object (数据单例) 是 Kotlin 1.9 中预定引入的新特性 &#xff0c;但其实从 1.7.20 开始就可以预览了。启动预览需要在 gradle 中升级 KotlinCompileVersion&#xff1a; tasks.withType<KotlinCompile> {kotlinOptions.languageVersion "1.9" }…

四、交换网络实验3——VTP配置

更多网络基础内容可见: 网络基础学习目录及各章节指引 4.6.3 VTP配置 实验目的 学习思科私有协议VTP的配置方法,观察VTP三种工作模式的通信方式 实验工具 Cisco Packet Tracer Student 软件 实验环境 安装模拟器的Windows系统 实验步骤 第一步:根据拓扑图,选择三台同…

老胡的周刊(第097期)

老胡的信息周刊[1]&#xff0c;记录这周我看到的有价值的信息&#xff0c;主要针对计算机领域&#xff0c;内容主题极大程度被我个人喜好主导。这个项目核心目的在于记录让自己有印象的信息做一个留存以及共享。 &#x1f3af; 项目 Chat2DB[2] Chat2DB 是一款有开源免费的智能…

二分查找--图文详解

二分查找 1. 什么是二分查找2. 原理3. 例子3.1 当数组长度为奇数3.1 当数组长度为偶数3.3 实现过程 4. 顺序查找与二分查找的区别结束语 1. 什么是二分查找 二分查找也称折半查找&#xff0c;是在一组有序(升序/降序)的数据中查找一个元素&#xff0c;它是一种效率较高的查找方…

chatgpt赋能python:重新配置PyCharm,让你的Python编程更加高效

重新配置PyCharm&#xff0c;让你的Python编程更加高效 PyCharm是一个流行的Python集成开发环境&#xff0c;被广泛用于Python编程。但是&#xff0c;有时候我们需要重新配置PyCharm以适应特定的工作需求或优化其性能&#xff0c;这篇文章将讨论如何重新配置PyCharm&#xff0…