preface

Often in nuggets to see a lot of students write multiple lines of text to omit how to handle, many are using CSS, these schemes more or less some compatibility and limitations.

My plan this year is to implement the React PC and mobile component libraries (mainly borrowed from (Chao) and copied from (XI) mature libraries). When I see ant Design and Arco Design’s Typography components, they include the processing of multiple lines of text omission. I looked at the source code (also sent a bug to Arco Desgin and raised a PR, ha ha) and explained the algorithm to everyone.

The core target

For example, our component is used like this

<Text ellipsis={{ rows: 2 }}>
      {mockText}
</Text>
Copy the code
  • Ellipsis is a configuration item with multiple lines of ellipsis,
    • Where rows:2 indicates the omission of two rows
  • MockText represents your full text, perhaps 100 lines

Core idea of algorithm

Problems we need to solve:

  • Truncate two lines, so we need to find out what text these two lines contain, and we need to truncate it!
    • Note: Consider the resize event, when the container width changes as you zoom in and out of the browser, the omission of two lines of text changes as well
  • How do I count the number of truncated lines

The difficulty of calculating the number of characters in the two lines is that the font Size and font Size of the container differ in the number of characters. So to figure out how many words are exactly two lines, we need to think differently.

Let’s use an example to illustrate the idea of transformation: suppose we use components this way

<Text ellipsis={{rows: 2}}> {here is 66 characters} </Text>Copy the code
  • Let’s say we have 66 lines of text, and the line height of the text is 22px (ignoring the padding and margin). Play is omitted.
  • Well, we can tell from the height, the height of the two rows is 22 times 2 = 44px.
  • We also know the total height of all the text. We can use the getComputedStyle API to get the scrollHight of the DOM, which is the total height of the text.
Window.getcomputedstyle (dom element ref)Copy the code
  • Before starting our algorithm, we need to determine whether the total height of the current text is less than or equal to 44px, and if so, whether your text does not meet the ellipsis situation, you have not omitted enough.

  • Now, this is where the ellipsis comes in.

  • If the total height of the 66 characters is greater than 44px, then 65 is equal to the last text of the two lines.

  • If the total height of 65 characters is still greater than 44px, then we can zoom out and see what the total height of 64 characters is, and so on, I, 63,62,61,61… I can always find boundary characters and the height of these characters is 44px.

Ok, up to where is the truncation, is the ellipsis thing solved.

The above algorithm still has some problems:

  • The ellipsis itself has a width, and the expanded text has a width, so we need to take that into account beforehand, by adding these characters to the DOM

  • How do we count 66,65,64… The height of these characters can’t be displayed directly on the page 66 characters, not two lines, delete one and calculate the height of 65 characters, so the user doesn’t go crazy, so we need one to exist on the page, just to calculate the height, and not to display the DOM element in the page

    • Create a DOM with the style fixed, top: -99999px and delete the DOM
  • It’s really inefficient to count them one by one. If you have 10,000 characters, you have to count 10,000 edges.

Let’s do a different algorithm, binary search, that is, at the beginning we calculate the height of half of the text, assuming that the height of this half is greater than 44px, that means that the border next to the ellipsis, the last text, is in this half.

Ok, let’s go ahead and take half of the text from the top half. If the height of the text is less than or equal to 44px, it means that the text is not enough. The real text is 44px in the other half.

By analogy, dichotomy can help us quickly find the boundary of the text, the algorithm time complexity is nlogn, or good performance.

Let’s take an example of how this dichotomy works:

// Assume that the entire text is as follows: // With some CSS style configuration it will appear as 3 lines<div id="w">
abcd
efgh
ijkl
</div>
Copy the code

We want to keep two lines so that (ignoring the ellipsis and the expand button for easy understanding) :

<div id="w">
adcd
efgh
</div>
Copy the code

The simple algorithm function is as follows:

const textNode = document.getElementById('w');
const originStyle = window.getComputedStyle(textNode);
const fullText = textNode.innerText;
// pxToNumber is a function that converts strings such as 12px to number12
const lineHeight = pxToNumber(originStyle.lineHeight); 
// Assume two rows remain
const rows = 2;
const maxHeight = Math.round( lineHeight * rows + pxToNumber(originStyle.paddingTop) + pxToNumber(originStyle.paddingBottom) );
// Determine if the current text height exceeds the height of two lines
const inRange  = function(){
    return textNode.scrollHeight <= maxHeight;
}
// The binary method finds the position of the last word in two lines of text
function measureText(textNode, startLoc = 0, endLoc = fullText.length) {
    const midLoc = Math.floor((startLoc + endLoc) / 2);
    const currentText = fullText.slice(0, midLoc);
    textNode.textContent = currentText;

    if (startLoc >= endLoc - 1) {
      for (let step = endLoc; step >= startLoc; step -= 1) {
        const currentStepText = fullText.slice(0, step);
        textNode.textContent = currentStepText;

        if(inRange() || ! currentStepText) {return; }}}if (inRange()) {
      return measureText(textNode, midLoc, endLoc);
    }
    return measureText(textNode, startLoc, midLoc);
  }
measureTex(textNode, 0The fullText. Length);Copy the code

Also, note that we do not need to consider the following form:

<Text ellipsis={{ rows: 2}} >123<span style="fontSzie: 12px">ab<span style="fontSzie: 22px">34</span>
</Text>
Copy the code

Because our actual algorithm will extract all the text and unify fontSize. Next, let’s take a look at the actual code of the framework source code, I will write the comments, this actually need not see, not easy to understand, just to say that some students want to explore its development environment code writing method, attached.

Here’s a quick AD: THIS year I’ll have a big theme that will teach you how to implement an open source react PC and mobile component library. It includes:

  • Cli tool for component library packaging, development, testing, and automation of push repository (including changeLog file modification and tagging) (100% complete)
  • The Icon package of the component library, that is, the collection of all ICONS in a service’s NPM package, is specific to your project.
  • Component library site building (write your own, not storybook, Dumi, or Docz)
  • PC side component library (includes all ant components and functions, mainly borrowed from its source code, also known as source code analysis)
  • Mobile component library (mainly borrowed from ZARM, a React component library of Zhongan)

Component libraries are source code analysis articles (I will improve the quality of the code on the basis of open source library source code).

import * as React from 'react';
// unmountComponentAtNode is the React API for uninstalling DOM elements
import { render, unmountComponentAtNode } from 'react-dom';
// mergedToString is a method that extracts all the text of its arguments
import mergedToString from '.. /_util/mergedToString';

// This is a simple way to merge style
function styleToString(style: CSSStyleDeclaration, extraStyle: { [key: string]: string } = {}) {
  const styleNames: string[] = Array.prototype.slice.apply(style);
  const styleString = styleNames
    .map((name) = > `${name}: ${style.getPropertyValue(name)}; `)
    .join(' ');
  const extraStyleString = Object.entries(extraStyle)
    .map(([key, value]) = > `${key}: ${value}; `)
    .join(' ');
  return styleString + extraStyleString;
}

// this is a function of px to number
function pxToNumber(value: string | null) :number {
  if(! value)return 0;

  const match = value.match(/^\d*(\.\d*)? /);

  return match ? Number(match[0) :0;
}

// The mirrored dom is the same dom we used to calculate height
let mirrorElement: HTMLElement;

export function measure(
  // This refers to the dom reference of the container element, i.e. the container enclosing our text. We need it to calculate the total height srcollHeight
  // Note that this is a DOM reference with all the text wrapped around it, so you can calculate the total height
  originElement: HTMLElement,
  // This is the configuration of the ellipsis information, we will encounter the next to explain the API
  ellipsisConfig,
  // This is the component that renders ellipses and expands and folds text. As we said above, we need to include them, because "... The "expand" itself also accounts for the width
  operations,
  // Package text message
  children,
) {
  // The default is to omit one line
  const rows = ellipsisConfig.rows || 1;
  We use "... "by default. , but you can also customize the omitted symbol
  constellipsisStr = ellipsisConfig.ellipsisStr ! = =undefined ? ellipsisConfig.ellipsisStr : '... ';

  // Create the DOM we used to calculate the height and add it to the page
  if(! mirrorElement) { mirrorElement =document.createElement(originElement.tagName);
    document.body.appendChild(mirrorElement);
  }
  // Calculates the final CSS properties of the wrap container
  const originStyle = window.getComputedStyle(originElement);

 // The CSS style used to hide the container we created. This is very specific, we pay attention to the comments
  const extraStyle = {
    // It is not clear why the height is forced to be auto, because it does not affect the scrollHeight value
    height: 'auto'.// Min-height and max-height are also unclear, probably due to browser compatibility issues
    'min-height': 'auto'.'max-height': 'auto'.left: '0'.top: '-99999999px'.position: 'fixed'.'z-index': '200'.'white-space': 'normal'.'text-overflow': 'clip'.overflow: 'auto'};/ / style
  const styleString = styleToString(originStyle, extraStyle);
  mirrorElement.setAttribute('style', styleString);
  mirrorElement.setAttribute('aria-hidden'.'true');
  // If ellipsis and expand the dom button, easy to calculate the width and height
  render(<span>{operations}</span>, mirrorElement);
    
   // This is not important, ignore, because you need to see opration generated code to understand
  const operationsChildNodes = Array.prototype.slice.apply(
    mirrorElement.childNodes[0].cloneNode(true).childNodes
  );
 // The rest of the code is our above example of a simple dichotomy idea
  const fullText = mergedToString(React.Children.toArray(children));
  unmountComponentAtNode(mirrorElement);
  mirrorElement.innerHTML = ' ';

  const ellipsisTextNode = document.createTextNode(`${ellipsisStr}${suffix}`);
  mirrorElement.appendChild(ellipsisTextNode);
  operationsChildNodes.forEach((childNode) = > {
    mirrorElement.appendChild(childNode);
  });

  const textNode = document.createTextNode(fullText);
  mirrorElement.insertBefore(textNode, mirrorElement.firstChild);

  const lineHeight = pxToNumber(originStyle.lineHeight);
  const maxHeight = Math.round(
    lineHeight * rows + pxToNumber(originStyle.paddingTop) + pxToNumber(originStyle.paddingBottom)
  );

  function emptyMirrorElem() {
    mirrorElement.setAttribute('style'.'display: none');
    mirrorElement.innerHTML = ' ';
  }

  function inRange() {
    return mirrorElement.scrollHeight <= maxHeight;
  }

  if (inRange()) {
    unmountComponentAtNode(mirrorElement);
    emptyMirrorElem();
    return { text: fullText, ellipsis: false };
  }

 
  function measureText(textNode: Text, startLoc = 0, endLoc = fullText.length, lastSuccessLoc = 0) {
    const midLoc = Math.floor((startLoc + endLoc) / 2);
    const currentText = fullText.slice(0, midLoc);
    textNode.textContent = currentText;

    if (startLoc >= endLoc - 1) {
      for (let step = endLoc; step >= startLoc; step -= 1) {
        const currentStepText = fullText.slice(0, step);
        textNode.textContent = currentStepText;

        if(inRange() || ! currentStepText) {return; }}}if (inRange()) {
      return measureText(textNode, midLoc, endLoc, midLoc);
    }
    return measureText(textNode, startLoc, midLoc, lastSuccessLoc);
  }

  measureText(textNode);
  const ellipsisText = textNode.textContent;

  emptyMirrorElem();
  return {
    text: ellipsisText,
    ellipsis: true}; }Copy the code