封装这个组件的时候,有人说, 这有什么必要吗? 几句css代码,就能搞定的事情,搞这么复杂干嘛, 我只想说,我也想让世界简单点, 奈何产品提需求提的我没法拒绝,确实有道理, 例如,
- 省略号在头部, 使用场景比如下面
// 前面按照规则生成的编号 只有后面有区别的 JSKKSJNDJKMDKDKMD-001 JSSMJKCLDJKMDKDKMD-003 JSKKNNHUJKMDKDKMD-002 想要的效果 ...JKMDKD-001 ...MDKDKM-002 ...MDKDKM-003
- 省略号在中间
我是超级长的我是超级长的我是超级长的我是超级长的我是的文件.jpg 我是超级长的我是超级长的我是超级长的我是超级长的我是的文件.png // 想要的效果 我是超级长的...文件.jpg 我是超级长的...文件.png
- 省略部分定制
- 悬浮显示气泡
这么一看 是不是就需要进行组件封装处理吧
首先先看下,我这个组件能达到的效果,看图
接下来我将一步步的展示我封装这个组件的过程, 以及收获了哪些知识
单行文本省略
这个很简单, 一句css搞定, 还无兼容性
.auto-ellipsis { white-space: nowrap; text-overflow: ellipsis; overflow: hidden; }
<div class="auto-ellipsis"> 超级长的文本超级长的文本超级长的文本超级长的文本超级长的文本 </div>
效果不贴了,省略
但是, 要改变省略号的位置,改到头部, 怎么玩.?
经过查资料, 发现以前没注意到的css属性direction
, 它有啥用呢, 看官方描述
看下面例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<style>
div {
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
white-space: nowrap;
}
</style>
</head>
<body>
<div>我啦哈哈洗西阿斯卡刷卡什么说的什么都没说房贷首付你说的那是大美女手机打开电视机打撒大厦酒店撒捷克丹麦京东商城明尼苏达是基础数据库导出</div>
</body>
</html>
效果如下
当时看到这, 瞬间精神一振, 搞定了,马上提交代码下班, 但当测试大哥提刀过来的时候,我感觉到不对劲了,怎么回事呢,往下看这个例子
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
div {
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
white-space: nowrap;
text-align: left;
}
</style>
</head>
<body>
<div>
11111185555555_1399319555555555555555555555555555555555555555555555555555555555555555555555555559751_18037893546_44444444
</div>
<div>
222222225555555_13993195554544444444444444444444444444445555555555555555555555555555555555555555559751_18037893546_5555555
</div>
<div>
3333333335555555_13993199555555555555555555555555555555555555555555555555555555555555555555555555751_18037893546_666666666
</div>
</body>
</html>
效果如下
有人会说, 没毛病呀,这不是效果杠杠滴么, 但仔细看我数字的顺序
根据效果图,看代码我圈住的地方
经查资料,发现这个属性针对纯数字,非纯数字文本的规则还不一样, 在不考虑超出隐藏的情况下, 看下面
<div>
11111_22222_33333_44444
</div>
<div>
11111 22222 33333 44444
</div>
<div>
aaaaa bbbbb ccccc dddddd eeeeee
</div>
<div>
aaaaa_11111_22222_33333_44444
</div>
// css
div {
width: 240px;
direction: rtl;
}
改变前
改变后
可以看到,这里非常核心的一点在于,对于纯数字的文本内容,数字的排列顺序也会跟着相应的书写顺序,进行反向排列!
而形如 11111_22222_33333_44444
这种用下划线连接的文本,处理的方式也会与 11111 22222 33333 44444
一样,实现了从左往右的排列,改变了原有的顺序。
怎么解决这个问题呢, 各位看到这里可以暂停一下,自己思考下有什么方案, 这里我就将我所了解到的方式了
- 两级反转, 顾名思义 我多来一层dom结构,再给你反转回来
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<style>
/* div {
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
white-space: nowrap;
text-align: left;
} */
.two-reversal {
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
white-space: nowrap;
text-align: left;
}
.two-reversal span {
direction: ltr;
}
</style>
</head>
<body>
<div class="two-reversal">
<span>
111111000085555555_1399319555555555555555555555555555555555555555555555555555555555555555555555555559751_18037893546_44444444
</span>
</div>
<div class="two-reversal">
<span>
222222225555555_13993195554544444444444444444444444444445555555555555555555555555555555555555555559751_18037893546_5555555
</span>
</div>
<div class="two-reversal">
<span>
3333333335555555_13993199555555555555555555555555555555555555555555555555555555555555555555555555751_18037893546_666666666
</span>
</div>
</body>
</html>
但是看到 还是不行
不着急, 接下来 就是
unicode-bidi: bidi-override
属性上场的时候了
加上看看
.two-reversal {
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
white-space: nowrap;
text-align: left;
}
.two-reversal span {
direction: ltr;
unicode-bidi: bidi-override;
}
发现ok了.
- 通过伪元素破坏其数字的连续性 具体啥意思呢, 就是我利用伪元素,往数字前或者后面插入一个字母, 这样就不是连续数字了, 就不会出现那种情况了
.add-sign {
overflow: hidden;
text-overflow: ellipsis;
direction: rtl;
white-space: nowrap;
span::before {
content: "a";
opacity: 0;
font-size: 0;
} }
- 使用通过
\200e
LRM 标记位,方式跟上面一样,只不过content的内容换成这个字符
通过 \200e
替换掉 a
,这里用 \200e
的目的与 a
的目的其实是不一样的:
-
在字符串前面通过伪元素添加一个
a
,目的是破坏其纯数字的特性 -
在字符串前面通过伪元素添加一个
\200e
,目的是强制控制接下来文本的排版顺序 -
使用标签
尽管同样的显示效果可以通过使用 CSS 规则
unicode-bidi
:隔离<span>
或者其他文本格式化元素,但语义信息只能通过元素传递。特别是,当浏览器允许忽略 CSS 样式时,在这种情况下,使用仍然可以保证文本正确显示,而使用 CSS 样式来传递语义时就显得毫无用处。
这部分内容参考了下面这位老哥的
超长溢出头部省略打点,坑这么大,技巧这么多? - 掘金 (juejin.cn)
多行文本
最常用的就是
.auto-ellipsis {
-webkit-line-clamp: 3;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
但是有个问题就是无法在非webkit内核的浏览器生效,所以这里从采用的是使用浮动来做到超出隐藏的效果
.demo {
max-height: 40px;
line-height: 20px;
overflow: hidden;
}
.demo::before {
float: left;
content: '';
width: 20px;
height: 40px;
}
.demo .text {
float: right;
width: 100%;
margin-left: -20px;
word-break: break-all;
}
.demo::after {
float: right;
content: '...';
width: 20px;
height: 20px;
position: relative;
left: 100%;
transform: translate(-100%, -100%);
background: #fff;
}
<div class="demo">
<div class="text">
That's the basic idea. You can imagine the light blue region as the title, and the yellow region as the ellipsis.
Then you may think that the pink box takes up space, but will the title be delayed as a whole? Here you can come
out by the negative value of margin. Set the negative value of the pale blue box to be the same width as the pink
box, and the title can also be displayed normally.
</div>
</div>
优点
- 无兼容问题
- 文本溢出范围才显示省略号,否则不显示省略号
缺点
- 省略号显示可能不会刚刚好,有时会遮住一半文字
- 适用于对省略效果要求较低的页面 后面省略号的背景可以加一个渐变来优化
background: linear-gradient(to right,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0.6) 5%,
rgba(255, 255, 255, 1) 40%);
中间省略效果
我是采用js来计算截取的, 本来是尾部也是, 但因为js计算字体宽度问题, 除非是等宽字体, 要不然总会存在偏差, 所以我的那个组件采用的是尾部使用css, 头部和中间 采用js计算截取
<template>
<div
:class="bem.b()"
ref="textEllipsis"
@mouseenter="mouseenter"
@mouseleave="mouseleave"
@click="$emit('ellipsis-click')"
>
<div :class="bem.e('before')" ref="textEllipsisBefore"></div>
<div :class="bem.e('text')" ref="text"><slot></slot></div>
<div :class="bem.e('after')" ref="textEllipsisAfter"></div>
</div>
</template>
<script>
import { createNamespace } from "@tcwl/utils";
export default {
name: "t-auto-ellipsis",
props: {
clamp: {
type: [String, Number],
default: 0,
},
// 省略号的位置 header body footer
direction: {
type: String,
default: "footer",
},
// 是显示省略号还是其他符号
mark: {
type: String,
default: "...",
},
// 定制省略部分的宽度
markWidth: {
type: [String, Number],
default: 25,
},
// 定制省略符号的颜色
markColor: {
type: String,
default: "#333",
},
// 是否显示气泡
isShowHover: {
type: Boolean,
default: false,
},
},
computed: {
isShowHoverValue: {
get() {
return this.isShowHover;
},
set(val) {
this.$emit("update:isShowHover", val);
},
},
},
watch: {
clamp: {
// immediate: true,
handler(val) {
this.clampNum = val * 1;
this.handleInit();
},
},
},
data() {
return {
bem: createNamespace("auto-ellipsis"),
textStyle: {},
div: null,
clampNum: this.clamp * 1,
};
},
mounted() {
setTimeout(() => {
this.handleInit();
}, 0);
window.addEventListener("resize", this.init());
},
beforeDestroy() {
window.removeEventListener("resize", this.init());
},
deactivated() {
window.removeEventListener("resize", this.init());
},
methods: {
handleInit() {
if (this.direction === "footer") {
this.getStyle();
} else {
this.init();
}
this.div = document.querySelector(".hover-wrap");
if (!this.div && this.clampNum) {
this.div = document.createElement("div");
this.div.className = "hover-wrap";
this.div.style.top = 0;
this.div.style.left = 0;
document.body.append(this.div);
}
},
async init() {
await this.$nextTick();
if (this.direction === "footer") {
return;
}
let fatherWidth = this.getBoxContentWidth(this.$el.parentElement);
let fontSize = getComputedStyle(this.$el.parentElement)[
"font-size"
].replace("px", "");
let fontFamily = getComputedStyle(this.$el.parentElement)[
"font-family"
];
let options = { size: fontSize, family: fontFamily };
let text = this.$slots.default[0].text;
let textWidth = Math.ceil(
this.getActualWidthOfChars(text, options)
);
let ellipsisWith = Math.ceil(
this.getActualWidthOfChars(this.mark, options)
);
let textNumber = this.$slots.default[0].text.length; // 文本个数
let overflowWidth = fatherWidth - textWidth; // 溢出的宽度
let avgStrWidth = Math.ceil(textWidth / textNumber); // 每个字的平均宽度
let boundaryValue = Math.ceil(ellipsisWith / avgStrWidth); // 省略号所占个数
let canFitStrNumber = Math.floor(
(fatherWidth * this.clampNum) / avgStrWidth
); // 父级可以容纳的个数
let shouldDelStrNumber =
textNumber - canFitStrNumber + boundaryValue; // 要删除的个数
let delIndex = shouldDelStrNumber / 2; // 要从中间删除
let startIndex = Math.ceil(textNumber / 2 - delIndex); // 计算需要截取的开始位置
let endIndex = Math.ceil(textNumber / 2 + delIndex); // 计算需要截取的结束位置
switch (this.direction) {
case "center": {
this.$refs.text &&
(this.$refs.text.innerHTML = `
${text.slice(0, startIndex)}<span style='color: ${
this.markColor
}'>${this.mark}</span>${text.slice(endIndex)}`);
break;
}
case "header": {
this.$refs.text &&
(this.$refs.text.innerHTML = `
<span style='color: ${this.markColor}'>${
this.mark
}</span>${text.slice(shouldDelStrNumber)}
`);
break;
}
}
},
async getStyle() {
await this.$nextTick();
let element = this.$refs.textEllipsis;
let lineHeight = getComputedStyle(
this.$el.parentElement
).lineHeight.replace("px", "");
let fatherFontSize = getComputedStyle(element).fontSize.replace(
"px",
""
);
console.log("lineHeight", lineHeight);
console.log("fatherFontSize", fatherFontSize);
if (lineHeight == "" || lineHeight == "normal") {
lineHeight = fatherFontSize;
}
console.dir(this.$el.parentElement);
let textEllipsisBefore = this.$refs.textEllipsisBefore;
let textEllipsisAfter = this.$refs.textEllipsisAfter;
let text = this.$refs.text;
// 设置包裹盒子的高度和行高
element && (element.style["line-height"] = lineHeight + "px");
if (this.clampNum != 0) {
element.style["max-height"] = this.clampNum * lineHeight + "px";
} else {
element.style["max-height"] = "none";
}
// 设置伪类before的样式 前后两个盒子的样式
if (textEllipsisBefore) {
textEllipsisBefore.style.cssText = `height: ${
this.clampNum * lineHeight
}px;width: ${this.markWidth}px`;
}
if (textEllipsisAfter) {
textEllipsisAfter.style.cssText = `height: ${lineHeight}px;width: ${this.markWidth}px`;
textEllipsisAfter.innerHTML = this.mark;
textEllipsisAfter.style.color = this.markColor;
textEllipsisAfter.style["text-align"] = "right";
}
if (text) {
text.style["margin-left"] = `-${this.markWidth}px`;
}
},
mouseenter(e) {
if (!this.isShowHoverValue) {
return;
}
let box = e.target.getBoundingClientRect();
e.target.style.cursor = "pointer";
let left = 0;
this.div.innerHTML = this.$slots.default[0].text;
left =
box.left +
(box.width - this.div.getBoundingClientRect().width) / 2 +
"px";
this.div.style.top =
box.top - this.div.getBoundingClientRect().height + "px";
this.div.style.left = left;
this.div.classList.add("active");
},
mouseleave(e) {
if (!this.isShowHoverValue) {
return;
}
if (e.relatedTarget !== this.div) {
// this.div.style.top = 0;
// this.div.style.left = 0;
this.div.classList.remove("active");
}
this.div.onmouseleave = () => {
// this.div.style.display = "none";
// this.div.style.top = 0;
// this.div.style.left = 0;
this.div.classList.remove("active");
};
},
// 计算文本宽度
getActualWidthOfChars(text, options = {}) {
const { size = 14, family = "Microsoft YaHei" } = options;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
ctx.font = `${size}px ${family}`;
const metrics = ctx.measureText(text);
const actual =
Math.abs(metrics.actualBoundingBoxLeft) +
Math.abs(metrics.actualBoundingBoxRight);
return Math.max(metrics.width, actual);
},
// 计算元素的content宽度
getBoxContentWidth(dom) {
let realityContentWidth = "";
let styleOption = getComputedStyle(dom);
let boxSizing = styleOption["boxSizing"];
let width = Math.ceil(styleOption["width"].replace("px", ""));
let padding = Math.ceil(
styleOption["padding-left"].replace("px", "") * 1 +
styleOption["padding-right"].replace("px", "") * 1
);
let borderStr = styleOption["border"];
const regex = /^(\d+)/;
const match = borderStr.match(regex);
let border = match ? match[1] : 0;
if (boxSizing === "border-box") {
realityContentWidth = width - padding - border;
} else {
realityContentWidth = dom.clientWidth || dom.offsetWidth;
}
return realityContentWidth;
},
},
};
</script>
<div class="base-ellipsis">
<p><b>单行省略(尾部)</b></p>
<t-auto-ellipsis clamp="1">
我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本
</t-auto-ellipsis>
<p><b>单行省略 + 定制省略符号(尾部)</b></p>
<t-auto-ellipsis
clamp="1"
mark="!!!"
markColor="red"
markWidth="35"
direction="footer"
>
我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本
</t-auto-ellipsis>
<p><b>两行省略(尾部)</b></p>
<t-auto-ellipsis clamp="2">
我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本
</t-auto-ellipsis>
<p><b>两行省略 + 去掉省略使用默认行为(尾部)渐隐效果</b></p>
<t-auto-ellipsis clamp="2" mark="">
我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本
</t-auto-ellipsis>
<p><b>三行省略(尾部)</b></p>
<t-auto-ellipsis clamp="3">
我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本我是老长的一段文本
</t-auto-ellipsis>
<p><b>单行省略(头部)</b></p>
<t-auto-ellipsis clamp="1" direction="header">
HHHHHHHHHHHHHHHHHHHH_JJJJJJJJJ_023565
</t-auto-ellipsis>
<p><b>单行省略(中间)</b></p>
<t-auto-ellipsis clamp="1" direction="center">
我是一个超长的文件名我是我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件一个超长的文件名我是一个超长的文件名我是一个超长的文件名我是一个超长的文件名我是一个超长的文件名我是一个超长的文件名.png
</t-auto-ellipsis>
<p><b>气泡显示</b></p>
<t-auto-ellipsis clamp="1" :isShowHover="true">
我是一个超长的文件名我是我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件我是一个超长的文件名我是一个超长的文件一个超长的文件名我是一个超长的文件名我是一个超长的文件名我是一个超长的文件名我是一个超长的文件名我是一个超长的文件名.png
</t-auto-ellipsis>
</div>
上面是组件源码和使用示例, 样式是上面说的浮动效果做的, 因为js设置伪元素样式存在问题, 所以我把伪元素弄成真实元素了
下面总结下当时不太熟悉的知识点吧
-
通过js动态改变伪元素before或after的content值
使用
data-*
给html设置数据属性,接着在css中通过attr()
获取html的属性值,再通过js修改属性值来改变content
<div class="box1 box2" data-before='原始值' ref='box' ></div>
.box1 {//初始化样式
&:before {
content: attr(data-before);
position: absolute;
left: 7px;
font-size: 14px;
color: #f44;
}
}
修改数据属性data-before的值,进而修改content值
读取方法:Element.getAttribute(name)
写入方法:Element.setAttribute(name, value)
- 通过js设置css属性
// 通常我们使用下面这种方式进行dom节点的样式设置
dom.style.color = 'red'
// 方法二 设置多条样式
dom.style.cssText = 'color: red;font-size: 10px'
// 方法三
dom.setAttribute('style', 'color: red;font-size: 10px')
// 方法四
dom.style.setProperty('color: red;font-size: 10px')
- 通过js给css的伪元素设置样式
let element = document.querySelector('.line-ellipsis')
const beforeStyle = window.getComputedStyle(element, '::before');
beforeStyle.setProperty('color', 'red');
// 但是上面的方法设置属性会存在浏览器兼容性, 有些熟属性为只读 不允许设置, 我只测了高度宽度不能
// 第二种方法 通过js来创建和添加内联样式表,并在其中设置伪元素样式
const style = document.createElement('style');
style.innerHTML = '.myElement::before { width: 100px; height: 50px; }';
document.head.appendChild(style);
// 但这种方式存在一个问题就是不能多次设置, style会进行覆盖
- 获取最近的父级dom元素
通常是使用$parent 来获取父组件, 但当你想获取父级的dom元素的时候, 使用这个就会出现问题,
比如在弹框中,获取回来的this.$parent.$el.clientWidth 可能就不是你想要的
使用这个 this.$el.parentElement.clientWidth 可获得正确的dom父级
-
计算文本宽度
已经是误差最小的算法了, 当然文本里面存在特殊字符 数字字母汉字的情况下还是会出现偏差
getActualWidthOfChars(text, options = {}) {
const { size = 14, family = 'Microsoft YaHei' } = options
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
ctx.font = `${size}px ${family}`
const metrics = ctx.measureText(text)
const actual = Math.abs(metrics.actualBoundingBoxLeft) + Math.abs(metrics.actualBoundingBoxRight)
return Math.max(metrics.width, actual)
}
- 计算元素的content的宽度
getBoxContentWidth(dom) {
let realityContentWidth = ''
let styleOption = getComputedStyle(dom)
let boxSizing = styleOption['boxSizing']
let width = Math.ceil(styleOption['width'].replace('px', ''))
let padding = Math.ceil(styleOption['padding-left'].replace('px', '')*1 + styleOption['padding-right'].replace('px', '')*1)
let borderStr = styleOption['border']
const regex = /^(\d+)/;
const match = borderStr.match(regex);
let border = match ? match[1] : 0
if (boxSizing === 'border-box') {
realityContentWidth = width - padding - border
} else {
realityContentWidth = dom.clientWidth || dom.offsetWidth
}
return realityContentWidth
}
当前组件存在的问题是下方, 后期会优化掉
- 当内容异步更新后可能会存在问题
- 内容后期规划成属性
- 屏幕尺寸发生变化的兼容问题
码字不易,如果看完对你有所帮助,请点个赞支持一下!