在前端开发中,多行文本的展开收起功能是一个常见且实用的交互设计。但是实现起来要考虑到情况诸多,符合应用的场景以及浏览器适配都有很多问题。借助CSS 实现多行文本“展开收起”多行文本展开收起是一个很常见的交互, 如下图演示 实现这一类布局和交互难点主要有以下几点 - 掘金 这篇详细分析css实现多行文本“展开/收起”, 在此基础上使用 Vue3 和 TypeScript 实现一个灵活、可复用的多行文本展开收起组件。
实现难点
在开始之前,让我们先了解实现多行文本展开收起功能的主要难点:
- 如何精确控制显示的行数
- 如何在文本末尾显示"展开"按钮
- 如何实现平滑的展开收起动画
- 如何处理文本不足指定行数的情况
TextEllipsis 组件实现
我们的 TextEllipsis 组件采用了纯 CSS 方案,结合 Vue3 的组合式 API,实现了一个灵活且易用的解决方案。下面是组件的核心代码:
<script setup lang="ts">
import { ref, computed, defineSlots } from 'vue'
interface Props {
text?: string
maxLines?: number
expandText?: string
collapseText?: string
shadowColor?: string
}
const props = withDefaults(defineProps<Props>(), {
text: undefined,
maxLines: 3,
shadowColor: '#ffffff',
expandText: '展开',
collapseText: '收起'
})
defineSlots<{
default?: () => string
toggle?: (props: { expanded: boolean; toggle: () => void }) => any
}>()
const expanded = ref(false)
const toggleId = ref(`exp-${Math.random().toString(36).slice(2, 11)}`)
const textRef = ref<HTMLElement | null>(null)
const buttonText = computed(() => expanded.value ? props.collapseText : props.expandText)
const lineHeight = 1.5
const maxHeight = computed(() => `${props.maxLines * lineHeight}em`)
const toggle = () => {
expanded.value = !expanded.value
}
</script>
<template>
<div class="text-ellipsis-wrapper">
<input :id="toggleId" class="text-ellipsis-exp" type="checkbox" v-model="expanded">
<div ref="textRef" class="text-ellipsis-text"
:style="{ '--max-lines': props.maxLines, '--line-height': lineHeight, '--max-height': maxHeight, '--shadow-color': props.shadowColor }">
<label class="text-ellipsis-label" :for="toggleId">
<slot name="toggle" :expanded="expanded" :toggle="toggle">
<button class="default-btn" @click="toggle">
{{ buttonText }}
</button>
</slot>
</label>
<slot :msg="text">{{ text }}</slot>
</div>
</div>
</template>
<style scoped>
.text-ellipsis-wrapper {
display: flex;
overflow: hidden;
border-radius: 8px;
padding: 15px;
}
.text-ellipsis-text {
font-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
text-align: justify;
position: relative;
line-height: var(--line-height);
max-height: var(--max-height);
transition: .3s max-height;
}
.text-ellipsis-text::before {
content: '';
height: calc(100% - 24px);
float: right;
}
.text-ellipsis-text::after {
content: '';
width: 999vw;
height: 999vw;
position: absolute;
box-shadow: inset calc(100px - 999vw) calc(30px - 999vw) 0 0 var(--shadow-color);
margin-left: -100px;
}
.text-ellipsis-label {
position: relative;
float: right;
clear: both;
margin-left: 20px;
}
.default-btn {
background: none;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
color: #1890ff;
}
.text-ellipsis-label::before {
content: '...';
position: absolute;
left: -5px;
color: #333;
transform: translateX(-100%)
}
.text-ellipsis-exp {
display: none;
}
.text-ellipsis-exp:checked+.text-ellipsis-text {
max-height: none;
}
.text-ellipsis-exp:checked+.text-ellipsis-text::after {
visibility: hidden;
}
.text-ellipsis-exp:checked+.text-ellipsis-text .text-ellipsis-label::before {
visibility: hidden;
}
</style>
React版本
import React, { useState, useRef, useEffect, useCallback } from 'react';
import './text-ellipsis.css';
interface TextEllipsisProps {
text: string;
maxLines?: number;
expandText?: string;
collapseText?: string;
shadowColor?: string;
children?: React.ReactNode;
}
const TextEllipsis: React.FC<TextEllipsisProps> = ({
text,
maxLines = 3,
expandText = '展开',
collapseText = '收起',
shadowColor = '#ffffff',
children,
}) => {
const [expanded, setExpanded] = useState(false);
const textRef = useRef<HTMLDivElement>(null);
const toggleId = `exp-${Math.random().toString(36).slice(2, 11)}`;
const lineHeight = 1.5;
const maxHeight = `${maxLines * lineHeight}em`;
const toggle = () => {
setExpanded(!expanded);
};
const buttonText = expanded ? collapseText : expandText;
return (
<div className='text-ellipsis-wrapper'>
<input
id={toggleId}
type='checkbox'
className='text-ellipsis-exp'
checked={expanded}
onChange={() => setExpanded(!expanded)}
/>
<div
ref={textRef}
className='text-ellipsis-text'
style={
{
maxHeight: expanded ? 'none' : maxHeight,
lineHeight: `${lineHeight}`,
'--shadow-color': shadowColor,
} as React.CSSProperties
}
>
<label htmlFor={toggleId} className='text-ellipsis-label'>
{children ? (
React.Children.map(children, child => {
if (React.isValidElement(child)) {
return React.cloneElement(child, { expanded, toggle } as any);
}
return child;
})
) : (
<button onClick={toggle} className='default-button'>
{buttonText}
</button>
)}
</label>
{text}
</div>
</div>
);
};
export default TextEllipsis;
.text-ellipsis-wrapper {
display: flex;
overflow: hidden;
border-radius: 8px;
padding: 15px;
}
.text-ellipsis-exp {
display: none;
}
.text-ellipsis-text {
font-size: 16px;
overflow: hidden;
text-overflow: ellipsis;
text-align: justify;
position: relative;
transition: 0.3s max-height;
}
.text-ellipsis-text::before {
content: '';
height: calc(100% - 24px);
float: right;
}
.text-ellipsis-text::after {
content: '';
width: 999vw;
height: 999vw;
position: absolute;
box-shadow: inset calc(100px - 999vw) calc(30px - 999vw) 0 0 var(--shadow-color);
margin-left: -100px;
}
.text-ellipsis-exp:checked ~ .text-ellipsis-text::after {
visibility: hidden;
}
.text-ellipsis-label {
position: relative;
float: right;
clear: both;
margin-left: 20px;
}
.text-ellipsis-label::before {
content: '...';
position: absolute;
left: -5px;
color: #333;
transform: translateX(-100%);
}
.default-button {
background: none;
border: none;
padding: 0;
font: inherit;
cursor: pointer;
outline: inherit;
color: #1890ff;
}
关键技术点解析
-
动态最大高度: 我们使用计算属性
maxHeight来动态设置文本容器的最大高度。const maxHeight = computed(() => `${props.maxLines * lineHeight}em`) -
CSS 变量应用: 我们使用 CSS 变量来动态设置样式,提高了组件的灵活性。
<div ref="textRef" class="text-ellipsis-text" :style="{ '--max-lines': props.maxLines, '--line-height': lineHeight, '--max-height': maxHeight, '--shadow-color': props.shadowColor }" > -
展开/收起切换: 使用
v-model绑定的 checkbox 来控制文本的展开和收起状态。<input :id="toggleId" v-model="expanded" class="text-ellipsis-exp" type="checkbox"> -
自定义插槽: 提供了默认插槽,允许用户自定义展开/收起按钮的样式和行为。
<slot name="toggle" :expanded="expanded" :toggle="toggle"> <button class="default-btn" @click="toggle"> {{ buttonText }} </button> </slot>
CSS 技巧
TextEllipsis 组件的 CSS 部分也包含了一些巧妙的技术:
.text-ellipsis-text::before {
content: '';
height: calc(100% - 24px);
float: right;
}
.text-ellipsis-text::after {
content: '';
width: 999vw;
height: 999vw;
position: absolute;
box-shadow: inset calc(100px - 999vw) calc(30px - 999vw) 0 0 var(--shadow-color);
margin-left: -100px;
}
.text-ellipsis-label::before {
content: '...';
position: absolute;
left: -5px;
color: #333;
transform: translateX(-100%)
}
- 使用
::before伪元素创建一个浮动的空间,用于放置展开/收起按钮。 - 使用
::after伪元素创建一个巧妙的渐变遮罩效果。 - 为标签添加一个
::before伪元素,显示省略号。
组件的使用
使用 TextEllipsis 组件非常简单:
- 直接使用
<template>
<TextEllipsis :text="longText" />
</template>
<script setup lang="ts">
import TextEllipsis from './components/TextEllipsis.vue'
const longText = '这是一段很长的文本...'
</script>
- 使用插槽
<template>
<TextEllipsis :max-lines="3">
{{ longText }}
<template #toggle="{ expanded, toggle }">
<button @click="toggle">
{{ expanded ? '收起' : '展开' }}
</button>
</template>
</TextEllipsis>
</template>
<script setup lang="ts">
import TextEllipsis from './components/TextEllipsis.vue'
const longText = '这是一段很长的文本...'
</script>
优势与特点
- 高度可定制:支持自定义最大行数、展开/收起文本、渐变颜色等。
- 灵活的插槽设计:允许用户自定义展开/收起按钮的样式和行为。
- 响应式设计:适应不同屏幕尺寸和字体大小。
总结
通过 TextEllipsis 组件,我们巧妙地解决了多行文本展开收起的实现难点。这个组件不仅功能强大,而且具有很高的可复用性和可定制性。在实际项目中,它可以大大提升用户体验,同时简化开发流程。
希望这篇文章能够帮助你更好地理解和实现多行文本展开收起功能。如果你有任何问题或改进建议,欢迎在评论区留言讨论!