在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;
      }
    }
  }
}
相关推荐
霸王蟹3 分钟前
带你手写React中的useReducer函数。(底层实现)
前端·javascript·笔记·学习·react.js·typescript·前端框架
托尼沙滩裤8 分钟前
【Vue3】实现屏幕共享惊艳亮相
前端·javascript·vue.js
啃火龙果的兔子13 分钟前
前端八股文-vue篇
前端·javascript·vue.js
孜然卷k19 分钟前
前端处理后端对象类型时间格式通用方法封装,前端JS处理JSON 序列化后的格式 java.time 包中的日期时间类
前端·json
幼儿园技术家23 分钟前
微信小程序实现用户进行推客的注册绑定
前端
gwcgwcjava26 分钟前
[技术积累]成熟的前端和后端开发框架
前端
bbsh209928 分钟前
SiteAzure:SetCookie 未设置Secure
前端·网络·安全·siteazure
Mintopia1 小时前
计算机图形学环境贴图(Environment Mapping)教学指南
前端·javascript·计算机图形学
码农之王1 小时前
(二)TypeScript前置编译配置
前端·后端·typescript
spmcor1 小时前
css 之 Flexbox 的一生
前端·css