TextClamp for Vue3.0(Vue3.0的文本展开收起组件)

呦!大家好,好久没有更新博客了,最近实现了一个一直想自己完成的一个东西,就是文本的展开收起组件,以前项目需要用到,自己实现一个又太繁琐,所以那个时候都是用的别人的轮子,现在自己尝试了一下,居然实现了,所以在这里向各位分享一下。(郑重声明,实现肯定有很多种方法,下面的只是我自己原创的方法,不喜勿喷)代码其实很简单,注释也很详细,具体的实现效果也还是不错的,现在在下面贴一下代码:

npm包的链接在这里:text-clamp-for-vue3 - npm (npmjs.com)

TextClamp.vue 

<script setup lang="ts">
import { addListener, removeListener } from 'resize-detector'
import { ref, onMounted, onUnmounted } from 'vue'
import $ from 'jquery'
const props = withDefaults(defineProps<{
    text: string; // 传入的文本,必传项
    buttonType?: 'oneLine' | 'tight'; // 展开收起按钮分为:1. oneLine:自身占据单行 2. tight:和文字紧密相邻
    maxLines?: number; // 设置的显示的行数
    isExpanded?: boolean; // 展开的状态,true:展开,false:收起
}>(), {
    buttonType: 'oneLine',
    isExpanded: false,
    maxLines: 3
})
const textClampRef = ref<HTMLElement | null>(null) // 最外层div的ref
// const textClampContainerRef = ref<HTMLElement | null>(null) // 该组件放内容的容器的ref
const textRef = ref<HTMLElement | null>(null) // 该组件放文本内容的ref
const toggleButtonRef = ref<HTMLElement | null>(null)
const expanded = ref(props.isExpanded) //本地的expanded状态,先获取一遍属性中的isExpanded状态,然后才能用toggle方法进行修改
const step = ref<number>(200) // 截取文本的位置
// 这个是按钮为单行的模式下的折叠文本的css
const clampClass = ref({
    'display': '-webkit-box',
    '-webkit-box-orient': 'vertical',
    '-webkit-line-clamp': `${props.maxLines}`,// 这里可以根据给定的行数设置具体的clamp行数,这也是我要将css封装成一个对象的目的
    'overflow': 'hidden',
    'text-overflow': 'ellipsis'
})
// 往右移动一个步长,注意我在这里将步长设置为了2,因为如果设置为1的时候,出现按钮不能紧密收紧的情况比较频繁,设置为2之后基本就正常了
function moveRight() {
    step.value = step.value + 2
}
// 往左移动一个步长
function moveLeft() {
    step.value = step.value - 2
}
/**
 * 获取一下展开收起按钮的宽度
 */
function getButtonWidth() {
    // 按钮
    const buttonElement = $('#textRefSpan').next()[0]
    // 文本容器
    const textContainer = textClampRef.value
    // 按钮的宽
    const buttonWidth = buttonElement.clientWidth
    // 按钮容器的宽
    const textContainerWidth = textContainer?.clientWidth
    // return出去给其他方法用
    return { buttonWidth, textContainerWidth }
}
/**
 * ?将文本进行折叠(只在按钮的类型为tight的时候发挥作用)
 * *具体实现思路:先随便假定一个截取范围,例如我这里给的是[0,200],然后截取完对比下rect的宽度和maxLines,若maxLines < rect.length,则左移;若maxLines > rect.length,则右移;若相等,则调用tightText()收紧文本
 */
function clampText(tag?: string) {
    // 要在expand为false即收起的状态时或者tag为'canClamp'时即’可以折叠‘时才执行这个函数的函数体,这样做的原因是文本有可能在展开的状态下用户调整了浏览器的宽度,这样的话,展开收起按钮可能会因为expand的变化而不断变化,这样体验就差了
    if (!expanded.value || tag == 'canClamp') {
        // 先让textRef的内容从0截取到step,step是我随便设置的,设置为200,你也可以设置为其他值,我的想法是先设置为200,然后再调整,然后再逐步收紧
        textRef.value && (textRef.value.textContent = props.text.slice(0, step.value) + '...')
        const rects = textRef.value?.getClientRects()
        if (rects) {
            console.log('rects:', rects);
            if (props.maxLines < rects?.length) {
                moveLeft();
            } else if (props.maxLines > rects?.length) {
                moveRight();
            } else {
                tightText()
                return
            }
            // 也需要递归调用一下自己,使文本的实际行数和给定的maxLines保持一致
            clampText();
        }
    }
}
/**
 * 收紧文本,使按钮紧贴文本的省略号
 */
function tightText() {
    // 获取一下按钮和textContainer的宽度
    const { buttonWidth, textContainerWidth } = getButtonWidth()
    // 获取一下文本有几行(即有几个矩形)
    const rects = textRef.value?.getClientRects()
    // 如果rects或者textContainerWidth都存在的话s
    if (rects && textContainerWidth) {
        // 判断一下最后一个矩形的宽度加上按钮的宽度是否小于等于容器的宽度,如果是的话,就直接return,这个时候按钮就会紧贴文本的最右边而不换行(因为这个方法递归了)
        if ((rects[rects.length - 1].width + buttonWidth <= textContainerWidth) && props.maxLines == rects?.length) {
            return
        }
        // 如果最后一个矩形的宽度加按钮的宽度会大于等于容器宽度,说明这个时候按钮如果放在文本的最末尾就会超出导致换行,所以下面要moveLeft()
        if (rects[rects.length - 1].width + buttonWidth >= textContainerWidth) {
            console.log('here:');
            moveLeft()
            // 使用moveLeft()往左边移动后,再设置一下文本
            textRef.value && (textRef.value.textContent = props.text.slice(0, step.value) + '...')
        } else {
            // 如果最后一个矩形的宽度加按钮的宽度会小于等于容器宽度,说明这个时候按钮如果放在文本的最末尾不会超出导致换行,但是有可能结尾空很多空间,所以下面要moveRight(),往右收紧一点
            moveRight()
            textRef.value && (textRef.value.textContent = props.text.slice(0, step.value) + '...')
        }
        // 递归调用一下自己,不断去收紧,前面有return,所以不会爆栈
        tightText()
    }
}
/**
 * 初始处理一下文本
 */
function init() {
    // 当按钮是tight时,什么也不做;若按钮是oneLine时,将显示maxLines那么多行的含省略号的样式加上
    props.buttonType == 'tight' ? (textRef.value && (textRef.value.textContent = props.text.slice(0, step.value) + '...')) : $('#textRefSpan').css(clampClass.value)
    // 获取当前文本有多少行
    const rects = textRef.value?.getClientRects()
    if (rects) {
        // 当给定的maxLines的行数要比真正文本的行数还要小时,此时需要进行文本的截取
        if (props.maxLines <= rects.length) {
            clampText()
        } else {
            // 此时maxLines大于或者等于真正的文本行数,此时无需截取,之前将所有文本显示出来就好
            /**
             * !顺带一提,当按钮类型为oneLine时,是直接执行的这个else,因为本函数的开头在按钮为oneLine时为TextRefSpan设置了style,这会导致getClientRects只能取到一个矩形,即使用了css省略后的矩形,此时直接将textRef的内容设置为原文即可,css会自动省略,显示给定的行数
              */
            textRef.value && (textRef.value.textContent = props.text)
        }
    }
}

/**
 * 切换展开收起的方法
 */
function toggle() {
    // 当按钮的类型是单行类型时
    if (props.buttonType == 'oneLine') {
        // 当前是折叠状态时,一点就变成展开状态
        if ($('#textRefSpan').attr('style') !== undefined) {
            $('#textRefSpan').removeAttr('style')
        } else {
            // 当前是展开状态,一点变成折叠状态
            $('#textRefSpan').css(clampClass.value)
        }
        expanded.value = !expanded.value
    } else {
        // 当按钮的类型是紧密类型时
        if (!expanded.value) {
            // 判断,若当前(即点击toggle之前)是收起的状态,那么需要将文本展开,显示未截取的原文本
            // 此时要将监听去掉,不然在mounted中的监听会让文本又变成省略的状态
            props.buttonType == 'tight' && removeListener(textClampRef.value as HTMLElement)
            textRef.value && (textRef.value.textContent = props.text)
        } else {
            // 若当前已经是展开了的状态了,那么需要对文本进行截取,调用截取方法
            clampText('canClamp')
        }
        // 切换一下展开收起的状态
        expanded.value = !expanded.value
        // 将监听加上
        props.buttonType == 'tight' && addListener(textClampRef.value as HTMLElement, () => {
            clampText()
        })
    }
}
onMounted(() => {
    init()
    // 当按钮的类型是tight时才启动这个监听器
    if (textClampRef.value && props.buttonType == 'tight') {
        addListener(textClampRef.value as HTMLElement, () => {
            clampText()
        })
    }
})
onUnmounted(() => {
    // 卸载的时候取消对textClampRef的监听
    if (textClampRef.value && props.buttonType == 'tight') {
        removeListener(textClampRef.value as HTMLElement)
    }
}
)
</script>

<template>
    <div ref="textClampRef">
        <!-- <div ref="textClampContainerRef"> -->
        <span ref="textRef" id="textRefSpan"></span>
        <slot ref="toggleButtonRef" name="textExpandButton" :toggle="toggle" :buttonType="buttonType"
            :isExpanded="expanded"></slot>
        <!-- </div> -->
    </div>
</template>
<style scoped></style>

调用方法看:App.vue

<script setup lang="ts">
import TextClamp from "./components/TextClamp.vue";
import Card from "./components/Card.vue";
import { ref, computed, onMounted } from "vue";
let str =
  "The ETH upgrade upgrade timeline has never been clear. The end of December last year, as well as June and August this year, are all hazy dates. This time,the core developers have provided a clear date, market confidence has increased, and the voice is loud, and Ethereum has been impacted. There aretwo voices on Ethereum 2.0 in the market, with hundreds of billions ofdollars at stake. The first is to be pessimistic, thinking that theintroduction of 2.0 with cheap gas fees and large processing capacity willallow more projects to settle in Ethereum, and that the expansion of ETH'sdemand would raise the price of ETH.The second option is to remain solidlybearish.Following 2.0, a large number of ETHs were freed, and mass sellingand homogenization rivalry became more intense.";
</script>

<template>
  <Card>
    <template #banner>
      <img src="./assets/image3.png" />
    </template>
    <template #description>
      <TextClamp :text="str" :buttonType="'tight'" :maxLines="4">
        <template #textExpandButton="props">
          <div v-if="props.buttonType == 'oneLine'" :style="{
            textAlign: 'left',
            cursor: 'pointer',
            display: 'flex',
            justifyContent: 'flex-end'
          }">
            <button @click="props.toggle">
              {{ props.isExpanded ? "Collapse" : "Expand" }}
            </button>
          </div>
          <button @click="props.toggle" v-else>
            {{ props.isExpanded ? "Collapse" : "Expand" }}
          </button>
        </template>
      </TextClamp>
    </template>
  </Card>
</template>

<style scoped>
.logo {
  height: 6em;
  padding: 1.5em;
  will-change: filter;
}

.logo:hover {
  filter: drop-shadow(0 0 2em #646cffaa);
}

.logo.vue:hover {
  filter: drop-shadow(0 0 2em #42b883aa);
}
</style>

大家可以自己下载下来跑一下看看:
源码的链接在这里https://github.com/KBKUN024/TextClamp-for-Vue3.0如果大家有更好的实现,或者对代码的有什么改进的话,非常欢迎提PR,如果对你有用,麻烦你给我一个star吧哈哈。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

在Ail Linux中手动配置IPv6

第一步&#xff0c;登录阿里云服务器控制台&#xff0c;在“概览”页面找到对应实例&#xff0c;然后单击实例ID。 第二步&#xff0c;在“实例详情”页面中的“网络信息”栏目中&#xff0c;可以发现“IPv6 地址”中没有数据&#xff0c;然后单击“专有网络”的专有网络ID。 第…

Pandas进阶修炼120题-第三期(金融数据处理,51-80题)

目录 往期内容&#xff1a;第一期&#xff1a;Pandas基础&#xff08;1-20题&#xff09;第二期&#xff1a;Pandas数据处理&#xff08;21-50题&#xff09; 第三期 金融数据处理51.使用绝对路径读取本地Excel数据方法一&#xff1a;双反斜杠绝对路径方法二&#xff1a;r 拓展…

TypeScript算法题实战——剑指 Offer篇(5)

目录 一、平衡二叉树1.1、题目描述1.2、题解 二、数组中数字出现的次数2.1、题目描述2.2、题解 三、数组中数字出现的次数 II3.1、题目描述3.2、题解 四、和为s的两个数字4.1、题目描述4.2、题解 五、和为s的连续正数序列5.1、题目描述5.2、题解 六、翻转单词顺序6.1、题目描述…

大数据技术之Hive2

目录标题 3、Hive 数据类型3.1 基本数据类型&#xff1a;3.2 集合数据类型&#xff1a;3.3 类型转化 4、DDL数据定义4.1 创建数据库4.2 查询数据库4.3 创建表4.4 管理表4.5 外部表4.6 管理表与外部表的相互转换4.7 分区表4.7.1 分区表基本操作4.7.2 分区表注意事项 4.7 修改表4…

小程序picker 在苹果手机不兼容 bug,按month时在iPhone 显示不正确及自动定位时间问题

如下图&#xff1a;点击弹出时间列表&#xff1a;日历控件点击选择显示1年1月 解决: 加上起始时间字段 <picker mode"date" value"{{date}}" start"1970-09-01" end"2030-09-01"></picker> 问题二&#xff1a; 还是&a…

leetcode 面试题 01.03. URL化

⭐️ 题目描述 &#x1f31f; leetcode链接&#xff1a;面试题 01.03. URL化 思路&#xff1a; 计算出空格的个数&#xff0c;我们可以知道最后一个字符的位置 endPos&#xff0c;再从后 end 向前遍历若不是空格正常拷贝&#xff0c;是空格则替换成 %20&#xff0c;最终当空格…

Unity 性能优化二:内存问题

目录 策略导致的内存问题 GFX内存 纹理资源 压缩格式 Mipmap 网格资源 Read/Write 顶点数据 骨骼 静态合批 Shader资源 Reserved Memory RenderTexture 动画资源 音频资源 字体资源 粒子系统资源 Mono堆内存 策略导致的内存问题 1. Assetbundle 打包的时候…

antd中的Cascader级联选择框怎么清空重置React

项目场景&#xff1a; React项目&#xff0c;使用antd中的Cascader级联选择框 问题描述&#xff1a; 通过其他按钮无法重置选择框中的项 原因分析&#xff1a;&#xff08;对应解决办法一和二&#xff09; 1、级联选择框的数据默认是根据options绑定的数组中的value值来进行…

深入浅出指南:Netty开发【NIO核心组件】

目录 ​Netty开发【NIO核心组件】 1.NIO基础概念 2.NIO核心组件 2.1.Channel&&Buffer简介 2.2.Selector 服务器的多线程版本 服务器的线程池版本 服务器的selector版本 2.3.Buffer 0.ByteBuffer的正确使用流程 1.ByteBuffer类型简介 2.ByteBuffer核心属性说…

【解惑笔记】树莓派+OpenCV+YOLOv5目标检测(Pytorch框架)

【学习资料】 子豪兄的零基础树莓派教程https://github.com/TommyZihao/ZihaoTutorialOfRaspberryPi/blob/master/%E7%AC%AC2%E8%AE%B2%EF%BC%9A%E6%A0%91%E8%8E%93%E6%B4%BE%E6%96%B0%E6%89%8B%E6%97%A0%E7%97%9B%E5%BC%80%E6%9C%BA%E6%8C%87%E5%8D%97.md#%E7%83%A7%E5%BD%95…

Qt6 Qt Quick UI原型学习QML第七篇

文章目录 效果演示QML语法 ClickableImageV2.qmlQML语法 EasingCurves.qml时钟小球滚动QML 源码## 时钟小球滚动QML解释 语法解释参考动画片动画元素应用动画可点击图像V2上升的物体第一个对象第二个对象第三个对象缓和曲线分组动画并行动画连续动画嵌套动画 效果演示 QML语法 …

orm(连接MySQL,增删改,创建表,样例)

1.启动数据库 mysql -u root -p password:(输入密码)2.创建数据库 create database stu DEFAULT CHARSET utf8 COLLATE utf8_general_ci;3.更改Django中settings.py文件配置 Django连接数据库&#xff1a; DATABASES {default: {ENGINE: django.db.backends.mysql,NAME: st…

一起学算法(插入排序篇)

概念&#xff1a; 插入排序&#xff08;inertion Sort&#xff09;一般也被称为直接插入排序&#xff0c;是一种简单的直观的排序算法 工作原理&#xff1a;将待排列元素划分为&#xff08;已排序&#xff09;和&#xff08;未排序&#xff09;两部分&#xff0c;每次从&…

QT 视图(view)模型(model)汇总

QStringListModel和QListView UI界面 widget头文件 #ifndef WIDGET_H #define WIDGET_H#include <QStringList> #include <QStringListModel> #include <QWidget>QT_BEGIN_NAMESPACE namespace Ui { class Widget; } QT_END_NAMESPACEclass Widget : publi…

认识 springboot 并了解它的创建过程 - 1

前言 本篇介绍什么是SpringBoot, SpringBoot项目如何创建&#xff0c;认识创建SpringBoot项目的目录&#xff0c;了解SpringBoo特点如有错误&#xff0c;请在评论区指正&#xff0c;让我们一起交流&#xff0c;共同进步&#xff01; 文章目录 前言1.什么是springboot?2.为什么…

COMSOL三维Voronoi图泰森多边形3D模型轴压模拟及建模教程

多晶体模型采用三维Voronoi算法生成&#xff0c;试件尺寸为150150300mm棱柱模型&#xff0c;对晶格指定五种不同材料&#xff0c;实现晶格间的差异性。 对试件进行力学模拟&#xff0c;下侧为固定边界&#xff0c;限制z方向的位移&#xff0c;上表面通过给定位移的方式实现轴…

应用开发者的疑问:大模型是银弹吗?

被当成银弹的大模型 ChatGPT 火了之后&#xff0c;大模型似乎被当成了真正的银弹&#xff0c;所有的体验问题都想通过大模型解决&#xff1a; 能不能和大模型对话订机票&#xff1f;自然语言生成 SQL&#xff0c;简化报表分析工作&#xff1f;大模型帮老年人操作软件&#xff…

rpc通信原理浅析

rpc通信原理浅析 rpc(remote procedure call)&#xff0c;即远程过程调用&#xff0c;广泛用于分布式或是异构环境下的通信&#xff0c;数据格式一般采取protobuf。 protobuf&#xff08;protocol buffer&#xff09;是google 的一种数据交换的格式&#xff0c;它独立于平台语…

第2章 逻辑分页、AutoFac注入、工作单元与仓储

1 CoreCms.Net.Model.ViewModels.Basics.IPageList<T> namespace CoreCms.Net.Model.ViewModels.Basics { ///<typeparam name"T">泛型类型实例(1个指定实体的类型实例)。</typeparam> /// <summary> /// 【逻辑分页列表--接口】 /// <…

qt添加图标

1.添加资源 选择QtWidgetsApp.qrc文件打开 添加图标文件路径 添加图标文件 2.按钮添加图标 图标路径为:/res/res/swicth.jpg &#xff08;1&#xff09;代码设置图标 ui.pushButton_OPen->setIcon(QIcon(":/res/res/swicth.jpg")); &#xff08;2&#xff09;属…