如何使用Javascript检查CSS是否截断文本

16

我正在尝试使用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。


第一个块被悬停时返回true!您是在特定的浏览器中测试吗? - Boudy hesham
@Boudyhesham 它在Chrome上不起作用。你是在哪个浏览器上运行的? - Nidhin Joseph
Chrome 版本 86.0.4240.111 - Boudy hesham
我也使用了86.0.4240.111版本,进行了双重和三重检查,但第一个div对我来说返回false。 - Nidhin Joseph
可以确认在Chrome上不起作用,但在Firefox 83.0b5上按预期工作。 - benmneb
显示剩余3条评论
3个回答

9
这里的问题在于,HTMLElement.offsetWidthElement.scrollWidth 都是四舍五入后的值。
在我的电脑上,你元素真正的内部宽度实际上是 300.40625px,但在 Chrome 中被取整为 300px
解决方案是使用返回浮点数值的 API,可惜这样的 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)及其getBoundingClientRect()方法已足够,然而,尽管这可以告诉您元素文本内容的实际大小,但它仅检查文本内容。如果滚动是由于边距引起的,它将无法正常工作。

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的特定行为:它们在Range.getClientRects()结果中公开呈现省略号的客户端矩形。
因此,在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>


小更新

自从这个CL之后,谷歌Chrome在起始区间为0的情况下不再公开省略号的边界框(在上面代码片段中的倒数第二个测试明显是这种情况)。
这意味着我们的解决方法在这种特殊情况下不再有效。


有趣的方法,我会进行调试以查看为什么在Firefox和Safari上从底部到顶部的第二个测试失败了。 - que1326
根据您回答中的链接,getClientRects 现在似乎已经得到了完整的浏览器支持。 - tettoffensive
@tettoffensive 当我回答这个问题时(甚至是很久之前),它已经得到了完整的浏览器支持,差异只涉及 Range#getClientRects() 如何处理呈现的省略号,只有 Chrome 会返回该渲染省略号的 DOMRect,而其他所有浏览器都会忽略它。 - Kaiido

6

试着使用

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;
}

虽然有点不规范,但它能够工作。


不确定我能否使用这种笨拙的方式,但是感谢你的时间,伙计。 - Nidhin Joseph
1
这假设被测试的元素在设置为 body 的直接子元素时,其所有样式仍然适用。 - Kaiido

4

Kaiido提到问题的根源在于offsetWidthscrollWidth反映的是四舍五入后的值,而省略号是基于浮点数值显示的。但他未能找到适合跨浏览器解决该问题的方法。

然而,将这个知识与see sharper's的修改版本结合起来,在我的测试中完美地运行,并且应该是可靠和跨浏览器的。

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>


然而,这似乎不适用于 OP 的情况,在该情况下,他们在要检查的元素内部有内部元素。如果我读得正确,我认为您的代码仅在目标元素内只有裸的 TextNodes 时才能正常工作。(而且,就像 see sharpers 的答案一样,这假定应用于目标上的样式仍将应用于克隆。) - Kaiido
哦,而且我的答案可以跨浏览器工作,但不幸的是它比应该的要复杂一些,但我认为这仍然是必须的。 - Kaiido
我不确定为什么内部元素会引起问题。我刚刚编辑了我的片段,并在文本周围添加了<a>标签,就像问题中一样,它仍然可以正常工作。我是否有所遗漏? - P Daddy
是的,这假设应用于目标的样式也适用于克隆,但需要更多的注意确保通过将克隆添加到父元素而不是body元素来实现。显然,有些情况下这是不够的(事实上,片段代码接近这些情况之一,因为它使用:nth-child CSS选择器),但我敢打赌,在绝大多数情况下,这将足以应用相同的样式,并且处理那些不适用的情况通常很容易,可以根据具体情况进行处理。 - P Daddy
好的,它不起作用是因为它没有改变temp元素的宽度。因此,对于任何没有width: auto的元素,这都不起作用,这可能是溢出元素中非常普遍的情况。 - Kaiido
显示剩余5条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接