在react中封装一个多行文本展开收起组件

演示

实现

实现步骤:

  1. 实现文本多行折叠并且显示省略号。
  2. 实现展开收起按钮的定位。
  3. 实现展开和收起。
  4. 实现当文本行数低于指定的行数时,不显示展开按钮。

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;
      }
    }
  }
}
相关推荐
undefined&&懒洋洋12 分钟前
Web和UE5像素流送、通信教程
前端·ue5
大前端爱好者2 小时前
React 19 新特性详解
前端
随云6322 小时前
WebGL编程指南之着色器语言GLSL ES(入门GLSL ES这篇就够了)
前端·webgl
随云6322 小时前
WebGL编程指南之进入三维世界
前端·webgl
寻找09之夏3 小时前
【Vue3实战】:用导航守卫拦截未保存的编辑,提升用户体验
前端·vue.js
非著名架构师3 小时前
js混淆的方式方法
开发语言·javascript·ecmascript
多多米10054 小时前
初学Vue(2)
前端·javascript·vue.js
敏编程4 小时前
网页前端开发之Javascript入门篇(5/9):函数
开发语言·javascript
柏箱4 小时前
PHP基本语法总结
开发语言·前端·html·php
新缸中之脑4 小时前
Llama 3.2 安卓手机安装教程
前端·人工智能·算法