演示
实现
实现步骤:
- 实现文本多行折叠并且显示省略号。
- 实现展开收起按钮的定位。
- 实现展开和收起。
- 实现当文本行数低于指定的行数时,不显示展开按钮。
1. 实现文本多行折叠并且显示省略号
这里我使用的是scss,通过循环生成了控制1到6行的文本限制class。
scss
@for $i from 1 to 6 {
:global(.line-#{$i}) {
display: -webkit-box;
-webkit-line-clamp: #{$i}; // 限制元素内文本的行数,
-webkit-box-orient: vertical; // 设置文本的排列方式为垂直方向。
text-overflow: ellipsis; // 当文本溢出时,显示省略号来表示文本被截断。
overflow: hidden;
}
}
2. 实现折叠收起按钮定位
定义一个组件,在children(也就是文本)的前面放一个元素 作为button,注意一定要放在文本的前面,这样待会才能定位准确。
tsx
import Style from "./index.module.scss";
import Button from "@comp/Button";
import { BUTTON_THEME } from "@enums/index";
export default function TextClamp({ children, line = 3 }) {
return (
<div className={Style["text-clamp-inner"]}>
<span className={[Style["text-inner"], `line-${line}`].join(" ")}>
<span className={Style["btn"]}>
<Button width="28px" height="13px" theme={BUTTON_THEME.BLUE_EMPTY}>
展开
</Button>
</span>
{children}
</span>
</div>
);
}
为按钮添加浮动:
scss
.text-clamp-inner {
display: flex;
.text-inner {
text-align: justify;
.btn {
float: right;
margin-left: 5px;
font-size: 10px;
}
}
}
目前的样式:
现在需要把浮动按钮精准的定位到底部。
为按钮的父元素添加伪元素::before(不能使用::after),并且同样浮动到右边,这个时候伪元素和按钮都在右上角,我们为按钮添加clear:both 清理浮动,让伪元素在上方,按钮在下方,并且设置伪元素的高度为父元素的高度 - 按钮的高度。(注意,这里.text-inner的父元素需要是浮动元素,不然伪元素高度设置为100% 会失效。)
scss
.text-clamp-inner {
display: flex;
.text-inner {
text-align: justify;
&::before {
content: "";
float: right;
height: calc(100% - 13px);
}
.btn {
float: right;
clear: both;
margin-left: 5px;
}
}
}
这样 按钮就成功定位到底部了:
3. 实现展开和收起
添加一个多选框控制文本展开收起的状态,并且在button中添加一个label指向多选框,这里我们使用react的一个Hook:useId获取到一个独一无二的id,这样可以防止多选框的id冲突。
tsx
import { useId, useMemo, useState } from "react";
import Style from "./index.module.scss";
import Button from "@comp/Button";
import { BUTTON_THEME } from "@enums/index";
export default function TextClamp({ children, line = 3 }) {
const id = useId();
const expId = useMemo(() => "exp-" + id, [id]);
return (
<div className={Style["text-clamp-inner"]}>
<input id={expId} type="checkbox" />
<span className={[Style["text-inner"], `line-${line}`].join(" ")}>
<span className={Style["btn"]}>
<Button width="28px" height="13px" theme={BUTTON_THEME.BLUE_EMPTY}>
<label className={Style["label-exp"]} htmlFor={expId}>
展开
</label>
<label htmlFor={expId} className={Style["label-collapse"]}>
收起
</label>
</Button>
</span>
{children}
</span>
</div>
);
}
然后编写css,通过多选框的:check伪类来控制不同的显示,当选中的时候,让限制text-inner行数的line-clamp设置为999解除限制,并且让收起的label显示,反之同样,让展开的按钮显示。
scss
.text-clamp-inner {
display: flex;
[id^="exp-"] {
position: fixed;
left: 9999px;
width: 0;
height: 0;
opacity: 0;
&:checked ~ .text-inner {
-webkit-line-clamp: 999;
}
&:checked ~ .text-inner .btn .label-collapse {
display: inline-block !important;
}
&:not(:checked) ~ .text-inner .btn .label-exp {
display: inline-block !important;
}
}
.text-inner {
text-align: justify;
&::before {
content: "";
float: right;
height: calc(100% - 13px);
}
.btn {
float: right;
clear: both;
margin-left: 5px;
.label-collapse,
.label-exp {
display: none;
font-size: 10px;
}
}
}
}
现在就已经能够实现展开和收起的控制了:
4.实现当文本行数低于指定的行数时,不显示展开按钮。
想要判断文字是否超出了指定的行数,最简单但得办法就是使用scrollHeight和clientHeight判断,当二者相同的时候就是没超出,scrollHeight > clientHeight的时候就是超出了,我们定义一个变量isShowBtn来控制按钮的显示。
tsx
import { useId, useMemo, useState } from "react";
import Style from "./index.module.scss";
import Button from "@comp/Button";
import { BUTTON_THEME } from "@enums/index";
export default function TextClamp({ children, line = 3 }) {
const id = useId();
const expId = useMemo(() => "exp-" + id, [id]);
const [isShowBtn, setIsShowBtn] = useState(false);
return (
<div className={Style["text-clamp-inner"]}>
<input id={expId} type="checkbox" />
<span
ref={(el) => {
if (el) {
setIsShowBtn(el.scrollHeight > el.clientHeight);
}
}}
className={[Style["text-inner"], `line-${line}`].join(" ")}
>
{isShowBtn && (
<span className={Style["btn"]}>
<Button width="28px" height="13px" theme={BUTTON_THEME.BLUE_EMPTY}>
<label className={Style["label-exp"]} htmlFor={expId}>
展开
</label>
<label htmlFor={expId} className={Style["label-collapse"]}>
收起
</label>
</Button>
</span>
)}
{children}
</span>
</div>
);
}
完整代码
tsx
import { useId, useMemo, useState } from "react";
import Style from "./index.module.scss";
import Button from "@comp/Button";
import { BUTTON_THEME } from "@enums/index";
export default function TextClamp({ children, line = 3 }) {
const id = useId();
const expId = useMemo(() => "exp-" + id, [id]);
const [isShowBtn, setIsShowBtn] = useState(false);
return (
<div className={Style["text-clamp-inner"]}>
<input id={expId} type="checkbox" />
<span
ref={(el) => {
if (el) {
setIsShowBtn(el.scrollHeight > el.clientHeight);
}
}}
className={[Style["text-inner"], `line-${line}`].join(" ")}
>
{isShowBtn && (
<span className={Style["btn"]}>
<Button width="28px" height="13px" theme={BUTTON_THEME.BLUE_EMPTY}>
<label className={Style["label-exp"]} htmlFor={expId}>
展开
</label>
<label htmlFor={expId} className={Style["label-collapse"]}>
收起
</label>
</Button>
</span>
)}
{children}
</span>
</div>
);
}
scss:
scss
.text-clamp-inner {
display: flex;
[id^="exp-"] {
position: fixed;
left: 9999px;
width: 0;
height: 0;
opacity: 0;
&:checked ~ .text-inner {
-webkit-line-clamp: 999;
}
&:checked ~ .text-inner .btn .label-collapse {
display: inline-block !important;
}
&:not(:checked) ~ .text-inner .btn .label-exp {
display: inline-block !important;
}
}
.text-inner {
text-align: justify;
&::before {
content: "";
float: right;
height: calc(100% - 13px);
}
.btn {
float: right;
clear: both;
margin-left: 5px;
.label-collapse,
.label-exp {
display: none;
font-size: 10px;
}
}
}
}