前言
多行文本省略并展开收起,这算是很常见的需求了。本文将结合具体的业务详细介绍各种情况下的实现,总有一款适合你的情景
先放其中一个效果图:在表格中需要多行文本展开收起:
一、文本省略
先介绍基本的文本省略的使用
1. 单行省略
<div class="text">
互联网信息服务(含发布网络广告);第二类增值电信业务中的信息服务业务(不含固定网电话信息服务和互联网信息服务);制作、发行动画片、专题、电视综艺,不得制作时政新闻及同类专题、专栏等广播电视节目(广播电视节目制作经营许可证有效期至2023年1月12日)
</div>
css样式
.text {
width: 228px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
}
2. 多行省略
.text {
width: 200px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
二、文本块省略
单行省略效果:
期待效果:展示不全时,整块不展示
关键点:span标签的display设为inline-block
.person-card__desc {
width: 200px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border: 1px solid #ccc;
}
span {
display: inline-block;
}
<div class="person-card__desc">
<span>负责人</span>
<span>经理</span>
<span>UI 设计师</span>
<span>前端工程师</span>
</div>
三、卡片-多行文本展开收起
先上效果
准备一个div
<div class="text">
<button class="btn">展开</button>
互联网信息服务(含发布网络广告);第二类增值电信业务中的信息服务业务(不含固定网电话信息服务和互联网信息服务);制作、发行动画片、专题、电视综艺,不得制作时政新闻及同类专题、专栏等广播电视节目(广播电视节目制作经营许可证有效期至2023年1月12日)
</div>
1. 定义文本样式
.text {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
border: 1px solid black;
width: 200px;
line-height: 20px;
}
2. 按钮右下角环绕效果
//通过伪元素占位,使按钮从右上角移到右下角
.text::before {
content: "";
float: right;
width: 0;
height: 40px;
}
//按钮右浮动并清除浮动效果
.btn {
float: right;
clear: both;
height: 20px;
line-height: 18px;
}
3. 动态高度
上面伪元素的占位高度是定死的,现在设置动态高度。用整个容器高度减去按钮的高度
.text::before {
content: "";
float: right;
width: 0;
height: calc(100% - 20px);
}
由于设置了100%,因此父元素必须有高度,这里采用flex布局,在text外再包一层wrap
<div class="wrap">
<div class="text">
<button class="btn">展开</button>
互联网信息服务(含发布网络广告);第二类增值电信业务中的信息服务业务(不含固定网电话信息服务和互联网信息服务);制作、发行动画片、专题、电视综艺,不得制作时政新闻及同类专题、专栏等广播电视节目(广播电视节目制作经营许可证有效期至2023年1月12日)
</div>
</div>
.wrap {
display: flex;
}
此时效果
除此以外,动态高度也可以采用负的margin来实现
.text::before {
content: "";
float: right;
width: 0;
/* height: calc(100% - 20px); */
height: 100%;
margin-bottom: -20px;
}
4. 其他浏览器的兼容处理
此时UI那边开始提bug,因为在 safari、firefox和搜狗浏览器中,上面的样式会失去效果。如果不考虑这些浏览器则采用display: -webkit-box;实现折行。可以采用max-height模拟实现文本截断
.text {
width: 200px;
overflow: hidden;
border: 1px solid black;
line-height: 20px;
max-height: 60px;
text-align: justify; //文字两端对齐,通过增大文字的间隙使两端对齐
}
采用伪元素实现省略号,跟在按钮之前
.btn {
float: right;
clear: both;
height: 20px;
line-height: 18px;
margin-left: 20px;
position: relative;
}
.btn::before {
content: "...";
position: absolute;
left: -10px;
color: #333;
transform: translateX(-100%);
}
此时,效果和上面一样,并且没有浏览器兼容问题
5. 展开和收起两种状态
利用 input type="checkbox" 进行css状态切换,首先加一个 input,然后把之前的 button 换成 label ,并且通过 for 属性关联起来
<div class="wrap">
<input type="checkbox" id="exp" class="exp">
<div class="text">
<label class="btn" for="exp">展开</label>
互联网信息服务(含发布网络广告);第二类增值电信业务中的信息服务业务(不含固定网电话信息服务和互联网信息服务);制作、发行动画片、专题、电视综艺,不得制作时政新闻及同类专题、专栏等广播电视节目(广播电视节目制作经营许可证有效期至2023年1月12日)
</div>
</div>
这样,在点击 label 的时候,实际上是点击了 input 元素,现在来添加两种状态,分别是只显示 3 行和不做行数限制
.exp:checked + .text {
max-height: none;
}
这时,可以点击展开和收起了。对于按钮内容需要动态修改,由展开改为收起,可以使用伪类content生成技术
<label class="btn" for="exp"></label><!--去除按钮文字-->
.btn::after {
content: "展开"; /*采用content生成*/
}
添加 :checked 状态
.exp:checked + .text .btn::after {
content: "收起";
}
兼容版本由于前面的省略号是模拟出来的,不能自动隐藏,所以需要额外来处理
.exp:checked + .text .btn::before {
visibility: hidden; /*在展开状态下隐藏省略号*/
}
6. 文本行数的判断
当文本较少时,此时是没有发生截断,也就是没有省略号的,但是“展开”按钮却仍然位于右下角,如何隐藏呢
css可以利用伪类进行覆盖,当需要隐藏时进行覆盖,显示时不覆盖。这里通过text::after实现
.text {
position: relative;
}
.text::after {
content: "";
width: 10px;
height: 10px;
position: absolute;
background: red;
}
可以看到当省略号出现时,红块消失(因为在文本框外,又设置了overflow: hidden;所以消失),当省略号消失时(高度足够时),红块在文本框内
给红块设置一个足够大的宽度
.text::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: red;
}
可以看到当展开时,红块把展开按钮给覆盖了,然后再修改下,将背景改成白色
.text::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: #fff;
}
这样在文本少或者全展开时可以隐藏展开按钮
如果希望点击后仍然可见,添加下:checked状态即可,在展开时隐藏覆盖层
.exp:checked + .text::after {
visibility: hidden;
}
最后再稍微美化下样式,将定义在text类上的盒子样式,定义到wrap上,并给按钮字体加颜色
.wrap {
display: flex;
width: 200px;
border: 1px solid black;
padding: 10px;
}
.text {
overflow: hidden;
line-height: 20px;
max-height: 60px;
text-align: justify;
position: relative;
}
.btn {
float: right;
clear: both;
height: 20px;
line-height: 18px;
margin-left: 20px;
position: relative;
color: #025cdc;
cursor: pointer;
}
7. 最终代码
<!DOCTYPE html>
<html>
<head>
<title>testD3_chp15_1.html</title>
<script type="text/javascript" src="https://d3js.org/d3.v5.min.js"></script>
<meta name="keywords" content="keyword1,keyword2,keyword3" />
<meta name="description" content="this is my page" />
<meta name="content-type" content="text/html; charset=GBK" />
<style>
.wrap {
display: flex;
width: 200px;
border: 1px solid black;
padding: 10px;
}
.text {
overflow: hidden;
line-height: 20px;
max-height: 60px;
text-align: justify;
position: relative;
}
.text::before {
content: "";
float: right;
width: 0;
/* height: calc(100% - 20px); */
height: 100%;
margin-bottom: -20px;
}
.text::after {
content: "";
width: 100%;
height: 100%;
position: absolute;
background: #fff;
}
.btn {
float: right;
clear: both;
height: 20px;
line-height: 18px;
margin-left: 20px;
position: relative;
color: #025cdc;
cursor: pointer;
}
.btn::before {
content: "...";
position: absolute;
left: -10px;
color: #333;
transform: translateX(-100%);
}
.btn::after {
content: "展开";
}
.exp:checked + .text .btn::after {
content: "收起";
}
.exp {
display: none;
}
.exp:checked + .text {
max-height: none;
}
.exp:checked + .text .btn::before {
visibility: hidden;
/*在展开状态下隐藏省略号*/
}
.exp:checked + .text::after {
visibility: hidden;
}
</style>
</head>
<body>
<div class="wrap">
<input type="checkbox" id="exp" class="exp" />
<div class="text">
<label class="btn" for="exp"></label>
互联网信息服务(含发布网络广告);第二类增值电信业务中的信息服务业务(不含固定网电话信息服务和互联网信息服务);制作、发行动画片、专题、电视综艺,不得制作时政新闻及同类专题、专栏等广播电视节目(广播电视节目制作经营许可证有效期至2023年1月12日)
</div>
</div>
</body>
</html>
四、表格-多行文本展开收起
此时,UI跑过来和我说,除了卡片需要多行折叠,在表格中也需要。先上效果图
思路:实现原理与上面卡片展开收起相同,只是换了业务情景,通过接口获取数据,并在表格内展示。因为有多个展开收起,因此input标签的id和label标签的for需要一一对应
可以用脚手架创建一个新的React项目,antd自行安装
引入antd样式,修改index.js
import 'antd/dist/antd.min.css'; //添加样式
修改App.js
import React, { useState, useEffect } from "react";
import { Table } from "antd";
import "./App.css";
const columns = [
{
title: "Name",
dataIndex: "name",
key: "name",
},
{
title: "Age",
dataIndex: "age",
key: "age",
},
{
title: "Address",
dataIndex: "address",
key: "address",
render: (text, { key }) => (
<div className="wrap">
<input type="checkbox" id={`exp_${key}`} className="exp" />
<div className="text">
<label className="btn more" htmlFor={`exp_${key}`}></label>
{text}
</div>
</div>
),
},
];
//模拟接口
const getData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{
key: "1",
name: "John Brown",
age: 32,
address:
"互联网信息服务(含发布网络广告);第二类增值电信业务中的信息服务业务(不含固定网电话信息服务和互联网信息服务);制作、发行动画片、专题、电视综艺,不得制作时政新闻及同类专题、专栏等广播电视节目(广播电视节目制作经营许可证有效期至2023年1月12日)",
more: false,
},
{
key: "2",
name: "Jim Green",
age: 42,
address:
"互联网信息服务(含发布网络广告);第二类增值电信业务中的信息服务业务(不含固定网电话信息服务和互联网信息服务);制作、发行动画片、专题、电视综艺,不得制作时政新闻及同类专题、专栏等广播电视节目(广播电视节目制作经营许可证有效期至2023年1月12日)",
more: false,
},
{
key: "3",
name: "Joe Black",
age: 32,
address:
"计算机软、硬件的开发;计算机系统服务;销售计算机软、硬件及辅助设备",
more: false,
},
]);
}, 200);
});
};
function App() {
const [tableData, setTableData] = useState([]);
useEffect(() => {
getData().then((res) => {
setTableData(res);
});
}, []);
return (
<div className="container">
<Table columns={columns} dataSource={tableData} bordered />
</div>
);
}
export default App;
修改App.css
.container{
width:600px
}
.wrap {
display: flex;
}
.text {
overflow: hidden;
line-height: 20px;
max-height: 60px;
text-align: justify;
position: relative;
}
.text::before {
content: "";
float: right;
width: 0;
height: 100%;
margin-bottom: -20px;
}
.text::after {
content: '';
width: 100%;
height: 100%;
position: absolute;
background: #fff;
}
.btn {
float: right;
clear: both;
height: 20px;
margin-left: 10px;
position: relative;
color: #025cdc;
cursor: pointer;
}
.btn::before {
content: "...";
position: absolute;
left: -5px;
color: #333;
transform: translateX(-100%);
}
.exp {
display: none;
}
.btn::after {
content: "展开";
}
.exp:checked + .text {
max-height: none;
}
.exp:checked+.text .btn::after {
content: '收起'
}
.exp:checked+.text .btn::before {
visibility: hidden;
/*在展开状态下隐藏省略号*/
}
.exp:checked+.text::after {
visibility: hidden;
}
五、表格-多行文本展示收起-悬停展示全部
UI那边又说折行的时候,需要鼠标悬浮展示全部。我心里:不是有展开按钮了吗,还要这个功能不是多次一举吗,嘴上:好的
方案一:title属性
修改下columns属性的内容
//原来
{text}
//改成
<span className="content" title={text}>
{text}
</span>
//并在App.css中添加文本样式悬停样式
.content:hover {
text-decoration: underline;
}
UI看了效果后直摇头,说浏览器默认效果太丑了,不行,需要实现他设计稿上的样式
方案二:通过js控制悬停组件tooltip
1. 修改text::after的高度
之前通过text::after在文本不需要折行时,覆盖展开按钮。之前高度设了一个较大值100%,由于这里需要计算文本内容的高度,需要把高度改为行高
.text::after {
content: "";
width: 100%;
//height: 100%;
height: 20px;
position: absolute;
background: #fff;
}
2. 创建组件,新建titlePopover.js
先安装styled-components和ahooks两个插件
新建titlePopover.js
import {
memo,
useLayoutEffect,
useRef,
useState,
useCallback,
useEffect,
} from "react";
import styled from "styled-components";
import { Popover } from "antd";
import { useDebounceFn } from "ahooks";
const TitlePopover = ({ text, id }) => {
const refs = useRef();
const [isPop, setIsPop] = useState(false); // 是否有弹框
const autoSize = useCallback(() => {
//内容折行
if (refs.current?.scrollHeight > refs.current?.clientHeight) {
setIsPop(true);
} else {
setIsPop(false);
}
}, []);
useLayoutEffect(autoSize, [autoSize]);
//防抖处理
const { run } = useDebounceFn(autoSize, {
wait: 500,
});
//缩放窗口时重新计算高度
useEffect(() => {
window.addEventListener("resize", run, false);
return () => {
window.removeEventListener("resize", run, false);
};
}, [run]);
return (
<Wrap>
<input type="checkbox" id={`exp_${id}`} className="exp" />
<div className="text" ref={refs}>
<label className="btn more" htmlFor={`exp_${id}`}></label>
{isPop ? (
<Popover content={text} placement="top">
{text}
</Popover>
) : (
text
)}
</div>
</Wrap>
);
};
export default memo(TitlePopover);
const Wrap = styled.div`
display: flex;
.text {
overflow: hidden;
line-height: 20px;
max-height: 60px;
text-align: justify;
position: relative;
}
.text::before {
content: "";
float: right;
width: 0;
height: 100%;
margin-bottom: -20px;
}
.text::after {
content: "";
width: 100%;
height: 20px;
position: absolute;
background: #fff;
}
.btn {
float: right;
clear: both;
height: 20px;
margin-left: 15px;
position: relative;
color: #025cdc;
cursor: pointer;
}
.btn::before {
content: "...";
position: absolute;
left: -5px;
color: #333;
transform: translateX(-100%);
}
.exp {
display: none;
}
.btn::after {
content: "展开";
}
.exp:checked + .text {
max-height: none;
}
.exp:checked + .text .btn::after {
content: "收起";
}
.exp:checked + .text .btn::before {
visibility: hidden;
/*在展开状态下隐藏省略号*/
}
.exp:checked + .text::after {
visibility: hidden;
}
.content:hover {
text-decoration: underline;
}
`;
3. 修改App.js
将原先的
{
title: "Address",
dataIndex: "address",
key: "address",
render: (text, { key }) => (
<div className="wrap">
<input type="checkbox" id={`exp_${key}`} className="exp" />
<div className="text">
<label className="btn more" htmlFor={`exp_${key}`}></label>
{text}
</div>
</div>
),
},
改为
import TitlePopover from "./titlePopover";
{
title: "Address",
dataIndex: "address",
key: "address",
render: (text, { key }) => <TitlePopover text={text} id={key} />,
},
六、问题
纯css方案,展开按钮是一直存在的,这就意味着它是一直占据着宽度的。会遇到文本不需要换行,而展开按钮换行了,因为文本后侧剩余宽度不足以展示全部的展开按钮
解决方案:将text的伪元素的高度拉高,设置margin-left,并用box-shadow代替background
//替换成
.text::after {
content: "";
width: 999vw;
height: 999vw;
position: absolute;
box-shadow: inset calc(100px - 999vw) calc(30px - 999vw) 0 0 #fff;
margin-left: -100px;
}
不足之处,下面多空了一行(这种情况比较少见,需要拖动窗口卡到那个距离上,才会出现),也破坏了上面的悬浮判断 element.scrollHeight > element.clientHeight,要悬停可采用方案一title属性
七、结语
如果产品比较注重性能,可以采用上面纯css的方式进行开发;我这边的产品比较注重UI,所以只能牺牲点性能采用js去实现,采用js可避开CSS方案这个不足点
于是我又开始新的一天码农搬砖生活,由于本文篇幅实在过长,所以js方案我放到下一篇来讲。等更新时我会把链接补充到下方
如果你有不理解的地方,欢迎在评论区留言;如果需要demo源代码的话,也可以留言,回头我把代码放到GitHub上;如果有其他业务情景,也可留言
码字不易,点赞支持!!!
巨人的肩膀:juejin.cn/post/696390…