多行超出展示省略号

156 阅读3分钟

超长显示省略号是很常见的功能,但是我第一次做哈哈,顺便记录下。

第一次用了css方案,但是iphone有兼容问题,所以放弃了

我只做了展开,没做收起,并且展开按钮在文本后面

然后扒了vant4实现方案,注意内容是支持换行的,也就是css需要加white-space: pre-wrap

先说下实现的步骤

  • 克隆dom并定位到屏幕外
  • 通过行高和padding判断是否超长显示省略号
  • 二分法查找分割点

DOM结构

<div ref="textRef" class="text-ellipsis">{{expanded?content:text}}<span
    class="text-ellipsis-action"
    @click.stop="onClickAction"
    v-if="hasAction"
    >{{ actionText }}</span></div>

因为用了white-space:pre-wrap 所以标签和内容间不能有空格,所以格式看起来很怪

但是vant4的源码是有空格的,格式正常,不知道做了什么处理,有知道的大佬帮忙解惑!

克隆DOM

const cloneContainer = () => {
  if (!this.textRef) return;

  const originStyle = window.getComputedStyle(this.textRef);
  const container = document.createElement("div");
  const styleNames: string[] = Array.prototype.slice.apply(originStyle);
  styleNames.forEach(name => {
    container.style.setProperty(name, originStyle.getPropertyValue(name));
  });

  container.style.position = "fixed";
  container.style.zIndex = "-9999";
  container.style.top = "-9999px";
  container.style.height = "auto";
  container.style.minHeight = "auto";
  container.style.maxHeight = "auto";

  container.innerText = this.content;
  document.body.appendChild(container);
  return container;
};

通过getComputedStyle获取所有样式,设置到container上,再定位到屏幕外方便后续二分法查找

是否需要展开

const { paddingBottom, paddingTop, lineHeight } = container.style;
const maxHeight = Math.ceil(
    (Number(this.rows) + 0.5) * this.pxToNum(lineHeight) +
    this.pxToNum(paddingTop) +
    this.pxToNum(paddingBottom)
);

if (maxHeight < container.offsetHeight) {
  this.hasAction = true;
  this.text = calcEllipsisText(container, maxHeight);
} else {
  this.hasAction = false;
  this.text = this.content;
}

rows代表超长的行数,0.5我猜是兼容用的?然后通过最大高度和实际高度比较判断是否需要展示省略号

二分法

const calcEllipse = () => {
    const tail = (left: number, right: number): string => {
      if (right - left <= 1) {
        return content.slice(0, left) + dots;
      }

      const middle = Math.round((left + right) / 2);

      container.innerText = content.slice(0, middle) + dots + this.actionText;

      if (container.offsetHeight > maxHeight) {
        return tail(left, middle);
      }

      return tail(middle, right);
    };

    container.innerText = tail(0, end);
  };

  calcEllipse();
  return container.innerText;
};

展开

onClickAction(event: MouseEvent) {
    this.expanded = !this.expanded;
    this.hasAction = false;
}

完整代码

<template>
  <div ref="textRef" class="text-ellipsis">{{expanded?content:text}}<slot v-if="hasAction"><span
    class="text-ellipsis-action"
    @click.stop="onClickAction"
    v-if="hasAction"
    >{{ actionText }}</span></slot></div>
</template>

<script lang="ts">
import { Vue, Component, Ref, Prop, Watch } from "vue-property-decorator";

@Component({})
export default class TextEllipsis extends Vue {
  @Ref() textRef: any;
  @Prop() content!: string;
  @Prop({ default: 5 }) rows!: number | string;
  @Prop({ default: "全文" }) actionText!: string;
  @Prop({ default: "end" }) position!: string;
  @Watch("content", { immediate: true }) contentHandler(newContent: string) {
    if (newContent) {
      setTimeout(() => {
        this.calcEllipsised();
      }, 300);
    }
  }

  hasAction = false;
  text = "";
  expanded = false;
  dots = "...";
  expandText = "";

  pxToNum(value: string | null) {
    if (!value) return 0;
    const match = value.match(/^\d*(\.\d*)?/);
    return match ? Number(match[0]) : 0;
  }
  onClickAction(event: MouseEvent) {
    this.expanded = !this.expanded;
    this.hasAction = false;
  }

  calcEllipsised() {
    // 克隆dom
    const cloneContainer = () => {
      if (!this.textRef) return;

      const originStyle = window.getComputedStyle(this.textRef);
      const container = document.createElement("div");
      const styleNames: string[] = Array.prototype.slice.apply(originStyle);
      styleNames.forEach(name => {
        container.style.setProperty(name, originStyle.getPropertyValue(name));
      });

      container.style.position = "fixed";
      container.style.zIndex = "-9999";
      container.style.top = "-9999px";
      container.style.height = "auto";
      container.style.minHeight = "auto";
      container.style.maxHeight = "auto";

      container.innerText = this.content;
      document.body.appendChild(container);
      return container;
    };

    // 二分法查找
    const calcEllipsisText = (container: HTMLDivElement, maxHeight: number) => {
      const { content, position, dots } = this;
      const end = content.length;

      const calcEllipse = () => {
        const tail = (left: number, right: number): string => {
          if (right - left <= 1) {
            if (position === "end") {
              return content.slice(0, left) + dots;
            }
            return dots + content.slice(right, end);
          }

          const middle = Math.round((left + right) / 2);

          if (position === "end") {
            container.innerText =
                content.slice(0, middle) + dots + this.actionText;
          } else {
            container.innerText =
                dots + content.slice(middle, end) + this.actionText;
          }

          if (container.offsetHeight > maxHeight) {
            if (position === "end") {
              return tail(left, middle);
            }
            return tail(middle, right);
          }

          if (position === "end") {
            return tail(middle, right);
          }

          return tail(left, middle);
        };

        container.innerText = tail(0, end);
      };

      const middleTail = (
          leftPart: [number, number],
          rightPart: [number, number]
      ): string => {
        if (
            leftPart[1] - leftPart[0] <= 1 &&
            rightPart[1] - rightPart[0] <= 1
        ) {
          return (
              content.slice(0, leftPart[0]) +
              dots +
              content.slice(rightPart[1], end)
          );
        }

        const leftMiddle = Math.floor((leftPart[0] + leftPart[1]) / 2);
        const rightMiddle = Math.ceil((rightPart[0] + rightPart[1]) / 2);

        container.innerText =
            this.content.slice(0, leftMiddle) +
            this.dots +
            this.content.slice(rightMiddle, end) +
            this.expandText;

        if (container.offsetHeight >= maxHeight) {
          return middleTail(
              [leftPart[0], leftMiddle],
              [rightMiddle, rightPart[1]]
          );
        }

        return middleTail(
            [leftMiddle, leftPart[1]],
            [rightPart[0], rightMiddle]
        );
      };

      const middle = (0 + end) >> 1;
      this.position === "middle"
          ? (container.innerText = middleTail([0, middle], [middle, end]))
          : calcEllipse();
      return container.innerText;
    };

    const container = cloneContainer();
    if (!container) return;
    const { paddingBottom, paddingTop, lineHeight } = container.style;
    const maxHeight = Math.ceil(
        (Number(this.rows) + 0.5) * this.pxToNum(lineHeight) +
        this.pxToNum(paddingTop) +
        this.pxToNum(paddingBottom)
    );

    if (maxHeight < container.offsetHeight) {
      this.hasAction = true;
      this.text = calcEllipsisText(container, maxHeight);
    } else {
      this.hasAction = false;
      this.text = this.content;
    }

    document.body.removeChild(container);
  }
}
</script>

<style scoped lang="scss">
.text-ellipsis {
  line-height: 42px;
  white-space: pre-wrap;
  overflow-wrap: break-word;
  font-size: 28px;
  color: #2a2c2e;
  margin-top: 18px;

  &-action {
    cursor: pointer;
    font-size: 28px;
    color: #0079f2;
  }
}
</style>