我正在尝试使用 JS 检测文本是否被截断。除了下面的边缘情况之外,这里提到的解决方案非常有效。正如您将注意到的,如果文本在视觉上被截断,则鼠标悬停时的第一个块将返回 false。
function isEllipsisActive(e) {
return (e.offsetWidth < e.scrollWidth);
}
function onMouseHover(e) {
console.log(`is truncated: ${isEllipsisActive(e)}`);
}
div.red {
margin-bottom: 1em;
background: red;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 300px;
cursor: pointer;
}
<h6>Hover mouse and watch for console messages.</h6>
<!-- should return true -->
<div class="red" onmouseover="onMouseHover(this)">
<a>Analytics reports comes through garbled. Plsss</a>
</div>
<!-- should return true -->
<div class="red" onmouseover="onMouseHover(this)">
<a>Analytics reports comes through garbled. Plsssssss</a>
</div>
<!-- should return false -->
<div class="red" onmouseover="onMouseHover(this)">
<a>Normal text</a>
</div>
我追求的解决方案是每当文本被 css 截断时函数就返回 true。
HTMLElement.offsetWidth
和
Element.scrollWidth
都是四舍五入的值。在我的电脑上,你的元素的真实内部宽度实际上是
300.40625px
300px
。这里的解决方案是使用返回浮点值的API,并且没有太多...
人们可能会忍不住检查内部
<a>
的
getBoundingClientRect().width
,这实际上适用于所有 OP 的情况,但这只适用于这些情况:向 div 添加填充,向这些添加边距
<a>
,或其他元素,它就坏了。
document.querySelectorAll( ".test" ).forEach( el => {
el.classList.toggle( "truncated", isEllipsisActive( el ) );
} );
function isEllipsisActive( el ) {
return el.firstElementChild.getBoundingClientRect().width > el.getBoundingClientRect().width;
}
div.test {
margin-bottom: 1em;
background: red;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 300px;
}
div.truncated {
background: green;
}
.margin-left {
margin-left: 225px;
}
<!-- should be green -->
<div class="test">
<a>Analytics reports comes through garbled. Plsss</a>
</div>
<!-- should be green -->
<div class="test">
<a>Analytics reports comes through garbled. Plsssssss</a>
</div>
<!-- should be green -->
<div class="test">
<a>Analytics</a><a> reports comes through garbled. Plsssssss</a>
</div>
<!-- should be green -->
<div class="test">
<a class="margin-left">Shorter text</a>
</div>
<!-- should be red -->
<div class="test">
<a>Normal text</a>
</div>
因此,人们可能会认为
Range方法就可以了,但是,虽然这能够告诉元素中text 内容的实际大小,但这仅检查 text 内容。如果滚动是由边距引起的,则不起作用。
document.querySelectorAll(".test").forEach( el => {
el.classList.toggle( "truncated", isEllipsisActive( el ) );
} );
function isEllipsisActive( el ) {
return el.scrollWidth !== el.offsetWidth ?
el.scrollWidth > el.offsetWidth :
checkRanges( el ); // Blink and Webkit browsers do floor scrollWidth
}
function checkRanges( el ) {
const range = new Range();
range.selectNodeContents( el );
const range_rect = range.getBoundingClientRect();
const el_rect = el.getBoundingClientRect();
// assumes ltr direction
return range_rect.right > el_rect.right;
}
div.test {
margin-bottom: 1em;
background: red;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 300px;
}
div.truncated {
background: green;
}
.margin-left {
margin-left: 225px;
}
.margin-right {
margin-right: 225px;
}
<!-- should be green -->
<div class="test">
<a>Analytics reports comes through garbled. Plsss</a>
</div>
<!-- should be green -->
<div class="test">
<a>Analytics reports comes through garbled. Plsssssss</a>
</div>
<!-- should be green -->
<div class="test">
<a>Analytics</a><a> reports comes through garbled. Plsssssss</a>
</div>
<!-- should be green -->
<div class="test">
<a class="margin-left">Shorter text</a>
</div>
<!-- should be green -->
<div class="test">
<a class="margin-right">Shorter text</a>
</div>
<!-- should be red -->
<div class="test">
<a>Normal text</a>
</div>
因此,我能想到的唯一解决方案依赖于 Chrome 特定行为:它们确实在
的结果中公开了渲染省略号的客户端矩形。因此,有一种方法可以确定,在 Chrome 中
text-overflow
属性并检查此 DOMRect 是否出现。但是,由于这是 Chrome 独有的行为,我们仍然需要检查 Safari 的范围边界框位置。
document.querySelectorAll(".test").forEach( el => {
el.classList.toggle( "truncated", isEllipsisActive( el ) );
} );
function isEllipsisActive( el ) {
return el.scrollWidth !== el.offsetWidth ?
el.scrollWidth > el.offsetWidth :
checkRanges( el ); // Blink and Webkit browsers do floor scrollWidth
}
function checkRanges( el ) {
const range = new Range();
range.selectNodeContents( el );
const range_rect = range.getBoundingClientRect();
const el_rect = el.getBoundingClientRect();
// assumes ltr direction
if( range_rect.right > el_rect.right ) {
return true;
}
// Following check would be enough for Blink browsers
// but they are the only ones exposing this behavior.
// first force ellipsis
el.classList.add( "text-overflow-ellipsis" );
// get all the client rects (there should be one for the ellipsis)
const rects_ellipsis = range.getClientRects();
// force no ellipsis
el.classList.add( "text-overflow-clip" );
const rects_clipped = range.getClientRects();
// clean
el.classList.remove( "text-overflow-ellipsis" );
el.classList.remove( "text-overflow-clip" );
// if the counts changed, the text is truncated
return rects_clipped.length !== rects_ellipsis.length;
}
/* 2 new clasess to force the rendering of ellipsis */
.text-overflow-ellipsis {
text-overflow: ellipsis !important;
}
.text-overflow-clip {
text-overflow: clip !important;
}
div.test {
margin-bottom: 1em;
background: red;
color: #fff;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
width: 300px;
}
div.truncated {
background: green;
}
.margin-left {
margin-left: 225px;
}
.margin-right {
margin-right: 225px;
}
<!-- should be green -->
<div class="test">
<a>Analytics reports comes through garbled. Plsss</a>
</div>
<!-- should be green -->
<div class="test">
<a>Analytics reports comes through garbled. Plsssssss</a>
</div>
<!-- should be green -->
<div class="test">
<a>Analytics</a><a> reports comes through garbled. Plsssssss</a>
</div>
<!-- should be green -->
<div class="test">
<a class="margin-left">Shorter text</a>
</div>
<!-- should be green -->
<div class="test">
<a class="margin-right">Shorter text</a>
</div>
<!-- should be red -->
<div class="test">
<a>Normal text</a>
</div>
小更新
如果起始范围是 0
,Chrome 不会暴露省略号的边界框(这显然是上面代码片段中倒数第二个测试的情况)。
这意味着我们的解决方法在这种特殊情况下不再适用。
通过提到击中要害,问题在于offsetWidth
和
scrollWidth
反映的是舍入值,而省略号是基于浮点值显示的。 但他无法找到合适的跨浏览器解决方案来解决这个问题。但是,将这些知识与 see Sharper 方法的修改版本相结合在我的测试中效果很好,并且应该可靠且跨浏览器。
function isEllipsisActive(e) {
const temp = e.cloneNode(true);
temp.style.position = "fixed";
temp.style.overflow = "visible";
temp.style.whiteSpace = "nowrap";
temp.style.visibility = "hidden";
e.parentElement.appendChild(temp);
try {
const fullWidth = temp.getBoundingClientRect().width;
const displayWidth = e.getBoundingClientRect().width;
return fullWidth > displayWidth;
} finally {
temp.remove();
}
}
function isEllipsisActive(e) {
const temp = e.cloneNode(true);
temp.style.position = "fixed";
temp.style.overflow = "visible";
temp.style.whiteSpace = "nowrap";
temp.style.visibility = "hidden";
e.parentElement.appendChild(temp);
try {
const fullWidth = temp.getBoundingClientRect().width;
const displayWidth = e.getBoundingClientRect().width;
return {
offsetWidth: e.offsetWidth,
scrollWidth: e.scrollWidth,
fullWidth,
displayWidth,
truncated: fullWidth > displayWidth
};
} finally {
temp.remove();
}
}
function showSize(element, props) {
const offset = element.nextElementSibling;
const scroll = offset.nextElementSibling;
const display = scroll.nextElementSibling;
const full = display.nextElementSibling;
const truncated = full.nextElementSibling;
offset.textContent = props.offsetWidth;
scroll.textContent = props.scrollWidth;
display.textContent = props.displayWidth;
const fixed = props.fullWidth.toFixed(3);
full.innerHTML = fixed.replace(
/\.?0+$/,
"<span class='invisible'>$&</span>"
);
truncated.textContent = props.truncated ? "✔" : undefined;
}
function showAllSizes() {
const query = ".container > .row:nth-child(n + 2) > *:first-child";
for (const element of document.querySelectorAll(query)) {
showSize(element, isEllipsisActive(element));
}
}
document.addEventListener("readystatechange", () => {
if (document.readyState !== "complete") {
return;
}
const width = document.getElementById("width");
width.addEventListener("change", () => {
document.querySelector(".container").style.gridTemplateColumns =
`${width.value}px repeat(5, auto)`;
showAllSizes();
});
showAllSizes();
});
* {
font-family: 'Roboto', sans-serif;
font-size: 14px;
}
.container {
display: inline-grid;
grid-template-columns: 295px repeat(5, auto);
gap: 8px;
padding: 8px;
border: 1px solid gray;
}
.container > .row {
display: contents;
}
.container > .row > * {
display: block;
border-width: 1px;
border-style: solid;
}
.container > .row:first-child > * {
font-weight: bold;
padding: 3px;
text-align: center;
border-color: gray;
background-color: silver;
}
.container > .row:nth-child(n + 2) > *:first-child {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
border: 1px solid steelblue;
background-color: lightsteelblue;
}
.container
> .row:nth-child(n + 2)
> *:nth-child(n + 2):not(:last-child) {
border-color: khaki;
background-color: lemonchiffon;
text-align: right;
}
.container
> .row:nth-child(n + 2)
> *:last-child {
text-align: center;
}
.container
> .row:nth-child(n + 2)
> *:last-child:not(:empty) {
border-color: darkgreen;
background-color: green;
color: white;
}
.container
> .row:nth-child(n + 2)
> *:last-child:empty {
border-color: firebrick;
background-color: crimson;
}
.invisible {
visibility: hidden;
}
.test {
margin-top: 8px;
}
input[type="number"] {
margin-top: 4px;
text-align: right;
}
input[type="number"]::-webkit-inner-spin-button,
input[type="number"]::-webkit-outer-spin-button {
opacity: 1;
}
<head>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet">
</head>
<div class="container">
<div class="row">
<span>Text</span>
<span>Offset</span>
<span>Scroll</span>
<span>Display</span>
<span>Full</span>
<span>Truncated</span>
</div>
<div class="row">
<span>
<a>Analytics reports comes through garbled. Plsss</a>
</span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div class="row">
<span>
<a>Analytics reports comes through garbled. Plsssssss</a>
</span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
<div class="row">
<span>
<a>Normal text</a>
</span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
</div>
</div>
<div class="test">
<strong>
Try changing the width up or down a few pixels.<br />
</strong>
<label>
Width:
<input type="number" id="width" value="295" min="10" max="400" size="4" />
</label>
</div>
function isEllipsisActive(e) {
var c = e.cloneNode(true);
c.style.display = 'inline';
c.style.width = 'auto';
c.style.visibility = 'hidden';
document.body.appendChild(c);
const truncated = c.offsetWidth >= e.clientWidth;
c.remove();
return truncated;
}
虽然很hacky,但很有效。
所示,就像检查一样简单:
node.scrollHeight > node.clientHeight
function isTextTruncated(node){
const truncated = node.scrollHeight > node.clientHeight
console.clear()
console.log("is truncated: ", truncated)
}
// observe resize
const resizeObserver = new ResizeObserver(m => isTextTruncated(m[0].target))
resizeObserver.observe(truncatedContainer, { attributes: true })
p {
--line-clamp: 4;
display: -webkit-box;
-webkit-line-clamp: var(--line-clamp);
-webkit-box-orient: vertical;
resize: horizontal;
hyphens: auto;
width: 300px;
min-width: 200px;
max-width: 90%;
overflow: hidden;
}
<p id='truncatedContainer'>
Lorem ipsum dolor sit amet, consectetur adipiscing elit,
sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.
Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris
nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor
</p>