How does JS determine that text has been ellipsis?

Big factory technology advanced front-end Node advanced
Click above for Programmer Growth Guide and follow the official account
Reply 1, join the advanced Node communication group

Foreword

If you want the text to be omitted with an ellipsis after it exceeds the width, just add the following css.

.ellipsis {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

3 lines of css are done, but a problem arises:

If we want to display the popper when the mouse is hovering over the text when the text is omitted, that is, when the text exceeds the specified width, how should we implement it?

CSS handles the omission for us, but JS doesn’t know when text is omitted, so we have to calculate it through JS.

Next, I’ll introduce several ways to implement JS calculation omission.

createRange

I found that the Element-plus form component has already implemented this function, so let’s learn its source code first.

Source code address:

https://github.com/element-plus/element-plus/blob/dev/packages/components/table/src/table-body/events-helper.ts

// Paste only the relevant ones
const cellChild = (event.target as HTMLElement).querySelector('.cell')
const range = document.createRange()
range.setStart(cellChild, 0)
range.setEnd(cellChild, cellChild.childNodes.length)
let rangeWidth = range.getBoundingClientRect().width
let rangeHeight = range.getBoundingClientRect().height
/** detail: https://github.com/element-plus/element-plus/issues/10790
* What went wrong?
* UI > Browser > Zoom, In Blink/WebKit, getBoundingClientRect() sometimes returns inexact values, probably due to lost
precision during internal calculations. In the example above:
* - Expected: 188
* - Actual: 188.00000762939453
*/
const offsetWidth = rangeWidth - Math.floor(rangeWidth)
if (offsetWidth < 0.001) {
  rangeWidth = Math.floor(rangeWidth)
}
const offsetHeight = rangeHeight - Math.floor(rangeHeight)
if (offsetHeight < 0.001) {
  rangeHeight = Math.floor(rangeHeight)
}




const { top, left, right, bottom } = getPadding(cellChild) // see below
const horizontalPadding = left + right
const verticalPadding = top + bottom
if (
  rangeWidth + horizontalPadding > cellChild.offsetWidth ||
  rangeHeight + verticalPadding > cellChild.offsetHeight ||
  cellChild.scrollWidth > cellChild.offsetWidth
) {
  createTablePopper(
    parent?.refs.tableWrapper,
    cell,
    cell.innerText || cell.textContent,
    nextZIndex,
    tooltipOptions
  )
}
// getPadding function in line 17 of the above code
const getPadding = (el: HTMLElement) => {
  const style = window.getComputedStyle(el, null)
  const paddingLeft = Number.parseInt(style.paddingLeft, 10) || 0
  const paddingRight = Number.parseInt(style.paddingRight, 10) || 0
  const paddingTop = Number.parseInt(style.paddingTop, 10) || 0
  const paddingBottom = Number.parseInt(style.paddingBottom, 10) || 0
  return {
    left: paddingLeft,
    right: paddingRight,
    top: paddingTop,
    bottom: paddingBottom,
  }
}

document.createRange() is a method in JavaScript that creates a Range object that represents a range in the document. Range objects are typically used to select a portion of a document and then operate on it.

it can:

  1. Set the selected text range: You can use the document.createRange() method to create a Range object, and use the setStart() and setEnd() methods to set it Select the starting and ending position of the text.

  2. Inserting new elements: You can use the document.createRange() method to create a Range object and the insertNode() method to insert new elements into the document at a specified position.

  3. Get the position of a specific element: You can use the document.createRange() method to create a Range object, and use the getBoundingClientRect() method to get the position and size information of the element in the document.

Here element uses getBoundingClientRect of the range object to obtain the width and height of the element. At the same time, because the obtained width and height value has many decimal places, element-plus makes a judgment and discards the decimal part if the decimal value is less than 0.001.

Next, let’s make a copy. You can adjust the width of the box to see whether there are ellipses on the page.

<div class="ellipsis box">
  Lorem ipsum dolor sit amet consectetur adipisicing elit.
</div>


<style>
  .ellipsis {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }


  .box {
    border: 1px solid gray;
    padding: 10px;
  }
</style>

Note here that we need to distinguish between clientWidth and offsetWidth, because we have now added a 1px border to the box, so offsetWidth = 1 * 2 (border width on the left and right sides) + clientWidth, so we use clientWidth here to represent the actual width of the box.

8919095812ab224fe08356d2351606c3.jpeg

const checkEllipsis = () => {
  const range = document.createRange();
  range.setStart(box, 0)
  range.setEnd(box, box.childNodes.length)
  let rangeWidth = range.getBoundingClientRect().width
  let rangeHeight = range.getBoundingClientRect().height
  const contentWidth = rangeWidth - Math.floor(rangeWidth)
  const { pLeft, pRight } = getPadding(box)
  const horizontalPadding = pLeft + pRight
  if (rangeWidth + horizontalPadding > box.clientWidth) {
    result.textContent = 'Ellipses exist'
  } else {
    result.textContent = 'The container is wide enough and there are no ellipsis'
  }
}

In this method, the elements and styles placed in the div are not restricted. For example, html written this way can still be calculated correctly.

<div class="ellipsis box">
  Lorem ipsum dolor sit amet consectetur adipisicing elit.
  <span style="font-size: large;">hello world</span>
  <span style="letter-spacing: 20px;">hello world</span>
</div>

Create a div to get simulated width

We can also get the actual width of the element without overflow:hidden by creating an almost identical div.

<div class="ellipsis box">
  Lorem ipsum dolor sit amet consectetur adipisicing elit.
</div>


<style>
  .ellipsis {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }


  .box {
    border: 1px solid gray;
    padding: 10px;
  }
</style>
const checkEllipsis = () => {
  const elementWidth = box.clientWidth;
  const tempElement = document.createElement('div');
  const style = window.getComputedStyle(box, null)
  tempElement.style.cssText = `
    position: absolute;
    top: -9999px;
    left: -9999px;
    white-space: nowrap;
    padding-left:${style.paddingLeft};
    padding-right:${style.paddingRight};
    font-size: ${style.fontSize};
    font-family: ${style.fontFamily};
    font-weight: ${style.fontWeight};
    letter-spacing: ${style.letterSpacing};
  `;
  tempElement.textContent = box.textContent;
  document.body.appendChild(tempElement);
  if (tempElement.clientWidth >= elementWidth) {
    result.textContent = 'Ellipses exist'
  } else {
    result.textContent = 'The container is wide enough and there are no ellipsis'
  }
  document.body.removeChild(tempElement);
}

When there are multiple dom elements in the box element, you have to create the dom recursively, or you can try cloneNode(true) to try cloning.

Create a block element to wrap the inline element

This method was learned from acro design vue and should be the simplest method. The key point is that the outer layer must be a block element, and the inner layer must be an inline element.

<div class="ellipsis box">
  <span class="content">
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.
    Elit.
  </span>
</div>


<style>
  .ellipsis {
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
  }


  .box {
    border: 1px solid gray;
    padding: 10px;
  }
</style>

Through the above processing of css and html, we can realize the ellipisis of the text in the box element. At the same time, since there is no overflow processing on span.content, the offsetWidth of the span is still constant.

const checkEllipsis = () => {
  const { pLeft, pRight } = getPadding(box)
  const horizontalPadding = pLeft + pRight
  if (box.clientWidth <= content.offsetWidth + horizontalPadding ) {
    result.textContent = 'Ellipses exist'
  } else {
    result.textContent = 'The container is wide enough and there are no ellipsis'
  }
}

Similarly, as long as the outer element is block and the inner element is inline, the DOM elements inside can be placed casually.

<div class="ellipsis box">
  <span class="content">
    Lorem ipsum dolor sit amet consectetur adipisicing elit. Lorem ipsum dolor sit amet consectetur adipisicing elit.
    Elit.
    <span style="font-size: large;">
      hello world
    </span>
    <span style="letter-spacing: 20px;">
      hello world
    </span>
  </span>
</div>

Comparison of methods

  1. Performance (personal subjective judgment) 3>1>2

  2. Level of worry (personal subjective judgment): 1>3>2

  3. Accuracy (personal subjective judgment): The accuracy of the three methods is almost the same. If I insist on comparing, I think it is 3>1>2

After that, I will look at other component libraries to see what good methods they have, and then add more. The front end is always doing these very small things, haha.

Author: Jiaqi coder
Link: https://juejin.cn/post/7262280335978741797

Node community


I have formed a Node.js community with a very good atmosphere. There are many Node.js friends in it. If you are interested in learning Node.js (you can also have plans in the future), we can do Node.js-related work together. Exchange, learn and build together. Add Koala friends below and reply “Node”.

4d1b4e15cfb3dfdab05db17b63403189.png

"Share, Like, Watch" to support