背景
其实一直想要实现这个文本展开收起功能,但是没时间,刚好这次做项目时有这个需求,所以就一并实现了,主要是使用 二分法 递归实现的。纯css也能实现,不过有很大的缺点,本文两种方法都会实现的。还会实现省略号在文本中间显示,而不是在文本末尾。
JS版本
<template>
<div class="content" ref="content">
{{ isEllipsis ? text : content }}
<span v-if="hasActionText" class="action" @click="chaneActionText">{{ actionText }}</span>
</div>
</template>
<script>
export default {
name: 'TextEllipsis',
props: {
// 内容
content: {
type: String,
default: ''
},
// 行数
rows: {
type: Number,
default: 2
}
},
data() {
return {
contentDomStyle: null, // 获取元素样式
cloneContentDom: null, // 创建一个克隆元素
text: '', // 内容
isEllipsis: false, // 是否省略
hasActionText: false, // 是否有展开/收起操作文本
actionText: '展开', // 展开/收起
maxHeight: 0 // 最大高度
}
},
mounted() {
this.isEllipsisText()
},
methods: {
chaneActionText() {
this.isEllipsis = !this.isEllipsis
this.actionText = this.actionText === '展开' ? '收起' : '展开'
},
/**
* 克隆内容DOM函数
* 该函数用于克隆一个内容DOM元素,包括其所有样式,并将其添加到文档体中
* 此函数主要用于在屏幕外创建一个内容的副本,以便进行某些操作而不影响用户界面
*/
cloneContentDomFn() {
// 获取元素所有样式
this.contentDomStyle = getComputedStyle(this.$refs.content)
// 伪数组转化为数组,遍历所有样式名
const styleNames = Array.from(this.contentDomStyle)
// 创建一个div
this.cloneContentDom = document.createElement('div')
// 遍历样式
styleNames.forEach((name) => {
// 获取样式值
const nameValue = this.contentDomStyle.getPropertyValue(name)
// 设置样式
this.cloneContentDom.style[name] = nameValue
})
// 设置元素的位置,让其不在屏幕内
this.cloneContentDom.style.position = 'fixed'
this.cloneContentDom.style.top = '-9999px'
// 设置高度,一定要设置,否则计算高度会出错
this.cloneContentDom.style.height = 'auto'
// 设置内容文本
this.cloneContentDom.innerText = this.content
// 将克隆的元素添加到body中
document.body.appendChild(this.cloneContentDom)
},
/**
* 计算截断文本的函数
* 该函数用于递归计算在给定的最大高度限制下,如何截断文本并添加省略号
* @param {number} start - 文本开始的索引
* @param {number} end - 文本结束的索引
* @returns {string} - 截断后的文本
*/
calcTruncationText(start, end) {
// 当start和end之间的差值小于等于1时,说明已经接近截断点,执行最终处理
if (end - start <= 1) {
// 移除克隆的DOM元素,清理资源
document.body.removeChild(this.cloneContentDom)
// 返回截断后的文本,添加省略号
return this.content.slice(0, start) + '...'
}
// 计算文本的中间位置,用于二分法查找截断点
const middle = Math.floor((start + end) / 2)
// 设置克隆的DOM元素的文本内容,包括文本、省略号和操作文本,始终是从0开始截取的
this.cloneContentDom.innerText = this.content.slice(0, middle) + '...' + this.actionText
// 获取克隆的DOM元素的高度
const { height } = getComputedStyle(this.cloneContentDom)
// 如果高度超过最大限制,说明还需要进一步截断文本
if (parseInt(height) > parseInt(this.maxHeight)) {
// 递归处理文本的前半部分
return this.calcTruncationText(start, middle)
} else {
// 递归处理文本的后半部分
return this.calcTruncationText(middle, end)
}
},
/**
* 检查文本是否需要显示省略号
*
* 此方法用于判断内容文本是否超过预定的高度,如果超过,则显示省略号并添加展开操作文本
* 它首先会克隆内容的DOM元素,然后通过比较克隆元素的高度和最大允许高度来决定是否需要省略文本
*/
isEllipsisText() {
// 克隆内容DOM元素,以便进行高度测量
this.cloneContentDomFn()
// 获取克隆内容DOM元素的高度和实际展示内容元素的行高
const { height } = getComputedStyle(this.cloneContentDom)
const { lineHeight } = this.contentDomStyle
// 计算内容的最大高度
this.maxHeight = parseFloat(lineHeight) * this.rows
// 判断内容高度是否超过最大高度
if (parseInt(height) > parseInt(this.maxHeight)) {
// 如果超过,设置省略标志和操作文本标志,并计算显示的省略文本
this.isEllipsis = true
this.hasActionText = true
this.text = this.calcTruncationText(0, this.content.length)
} else {
// 如果不超过,清除省略标志和操作文本标志
this.isEllipsis = false
this.hasActionText = false
}
}
}
}
</script>
<style lang="scss" scoped>
.content {
.action {
cursor: pointer;
color: #bc2159;
}
}
</style>
需要注意的时
- 克隆的元素高度一定要设为 auto,否则高度计算可能不正确
- 如果需要改变按钮的样式,要看会不会影响按钮的宽高,如果影响则需要调整相关的代码
css版本
<template>
<!-- 外层容器,用于布局和样式控制 -->
<div class="wrapper">
<!-- 隐藏的复选框,用于控制文本的展开与收起 -->
<input id="exp1" class="exp" type="checkbox" />
<!-- 文本容器,包含可展开/收起的文本内容 -->
<div class="text">
<!-- 展开/收起按钮,通过label关联复选框 -->
<label class="btn" for="exp1"></label>
<!-- 动态显示的内容 -->
{{ content }}
</div>
</div>
</template>
<script>
// 定义一个名为TextEllipsis的组件
export default {
name: 'TextEllipsis',
props: {
// 接受一个名为content的字符串属性,用于显示文本内容
content: {
type: String,
default: ''
}
}
}
</script>
<style lang="scss" scoped>
// 设置组件样式,实现文本展开/收起效果
.wrapper {
display: flex;
line-height: 24px;
.exp {
display: none;
// 当复选框被选中时,文本不限制行数,实现展开效果
&:checked + .text {
-webkit-line-clamp: 999;
}
// 当复选框被选中时,按钮显示“收起”文本
&:checked + .text .btn::before {
content: '收起';
}
}
.text {
overflow: hidden;
text-overflow: ellipsis;
text-align: justify;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
position: relative;
// 使用伪元素保持空隙,以便放置展开/收起按钮
&::before {
content: '';
height: calc(100% - 24px);
float: right;
}
.btn {
float: right;
clear: both;
color: #bc2159;
cursor: pointer;
// 默认显示“展开”文本
&::before {
content: '展开';
}
}
}
}
</style>
可以看出css实现的与js实现的是有差别的,css的文本按钮只能在右下角,如果需要这种效果则可以使用,js实现的按钮则在文本末尾,并且css实现的最大的缺点就是在ios上,文本不能显示,会有兼容性!!!
省略号在文本中间显示
<template>
<div class="content" ref="content">
{{ isEllipsis ? text : content }}
</div>
</template>
<script>
export default {
name: 'TextEllipsis',
props: {
// 内容
content: {
type: String,
default: ''
},
// 行数
rows: {
type: Number,
default: 2
}
},
data() {
return {
contentDomStyle: null, // 获取元素样式
cloneContentDom: null, // 创建一个克隆元素
text: '', // 内容
isEllipsis: false, // 是否省略
maxHeight: 0 // 最大高度
}
},
mounted() {
this.isEllipsisText()
},
methods: {
/**
* 克隆内容DOM函数
* 该函数用于克隆一个内容DOM元素,包括其所有样式,并将其添加到文档体中
* 此函数主要用于在屏幕外创建一个内容的副本,以便进行某些操作而不影响用户界面
*/
cloneContentDomFn() {
// 获取元素所有样式
this.contentDomStyle = getComputedStyle(this.$refs.content)
// 伪数组转化为数组,遍历所有样式名
const styleNames = Array.from(this.contentDomStyle)
// 创建一个div
this.cloneContentDom = document.createElement('div')
// 遍历样式
styleNames.forEach((name) => {
// 获取样式值
const nameValue = this.contentDomStyle.getPropertyValue(name)
// 设置样式
this.cloneContentDom.style[name] = nameValue
})
// 设置元素的位置,让其不在屏幕内
this.cloneContentDom.style.position = 'fixed'
this.cloneContentDom.style.top = '-9999px'
// 设置高度,一定要设置,否则计算高度会出错
this.cloneContentDom.style.height = 'auto'
// 设置内容文本
this.cloneContentDom.innerText = this.content
// 将克隆的元素添加到body中
document.body.appendChild(this.cloneContentDom)
},
/**
* 计算截断文本的函数
*
* 该函数的目的是根据指定的左、右索引范围,截断长文本并返回一个缩短后的版本
* 它通过递归方式逼近满足最大高度限制的文本长度,同时确保文本的连贯性
*
* @param {Array} left - 左侧索引范围,用于确定文本截断的起始位置
* @param {Array} right - 右侧索引范围,用于确定文本截断的结束位置
* @returns {string} - 返回截断后的文本字符串
*/
calcTruncationText(left, right) {
// 当左右两侧的索引范围都满足条件时,执行文本截断并返回结果
if (left[1] - left[0] <= 1 && right[1] - right[0] <= 1) {
// 移除克隆的DOM元素,清理资源
document.body.removeChild(this.cloneContentDom)
// 返回中间截断的文本,用省略号连接左右两部分
return this.content.slice(0, left[0]) + '...' + this.content.slice(right[1])
}
// 计算左右两侧的中间索引
const leftMiddle = Math.floor((left[0] + left[1]) / 2)
const rightMiddle = Math.floor((right[0] + right[1]) / 2)
// 更新克隆的DOM元素的文本内容,用省略号连接左右两部分
this.cloneContentDom.innerText =
this.content.slice(0, leftMiddle) + '...' + this.content.slice(rightMiddle)
// 获取克隆的DOM元素的高度
const { height } = getComputedStyle(this.cloneContentDom)
// 根据高度判断是否需要进一步截断文本
if (parseInt(height) > parseInt(this.maxHeight)) {
// 如果高度超过最大限制,递归处理左侧范围缩小的情况
return this.calcTruncationText([left[0], leftMiddle], [rightMiddle, right[1]])
} else {
// 如果高度未超过最大限制,递归处理右侧范围缩小的情况
return this.calcTruncationText([leftMiddle, left[1]], [right[0], rightMiddle])
}
},
/**
* 检查文本是否需要显示省略号
* 此函数用于判断内容的高度是否超过预设的最大高度,从而决定是否需要显示省略号
*/
isEllipsisText() {
// 克隆内容DOM元素,以便进行操作而不影响原始元素
this.cloneContentDomFn()
// 获取克隆内容DOM的高度和实际显示内容元素的行高
const { height } = getComputedStyle(this.cloneContentDom)
const { lineHeight } = this.contentDomStyle
// 计算内容的最大高度
this.maxHeight = parseFloat(lineHeight) * this.rows
// 判断内容高度是否超过最大高度
if (parseInt(height) > parseInt(this.maxHeight)) {
// 如果内容高度超过最大高度,则设置isEllipsis为true,并计算显示的文本
this.isEllipsis = true
const middle = Math.floor(this.content.length / 2)
this.text = this.calcTruncationText([0, middle], [middle, this.content.length])
} else {
// 如果内容高度未超过最大高度,则设置isEllipsis为false
this.isEllipsis = false
}
}
}
}
</script>
<style lang="scss" scoped>
.content {
.action {
cursor: pointer;
color: #bc2159;
}
}
</style>
思路其实和js实现的思路差不多,只是现在左右位置使用的是数组