超长显示省略号是很常见的功能,但是我第一次做哈哈,顺便记录下。
第一次用了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>