JavaScript realizes text overflow and automatically reduces the font to N lines to adapt to the container

order

When a page needs to switch between different languages, the text length of the same sentence copy and different languages is difficult to be consistent. Sometimes, the unrestricted line feed display of the text will affect the whole page layout. Therefore, some methods need to be used to ensure that the layout of the text container is not deformed in different languages, that is, the text is limited to N lines, Ensure that the container is not opened or overflowed, and display more text in the limited space as much as possible.

(at most one line of text is given in the design draft)

(the actual page becomes two lines under the English copy)
The following methods are usually used to limit the text to N lines:

  1. If the text exceeds N lines, a scroll bar will be displayed
  2. If the text exceeds N lines, an ellipsis is displayed
  3. If the text exceeds N lines, the font size is set to a smaller font size. If it still exceeds, an ellipsis is displayed
  4. If the text exceeds N lines, the font size is reduced to just full of the container, and the minimum is reduced to a certain font size. If it still exceeds, an ellipsis is displayed

The first and second methods can be done by setting html and css. It is relatively simple. This paper mainly discusses the feasibility and implementation process of the third and fourth methods.

In order to facilitate operation, first put a span label on the text and then put it into the container, such as < div > < span > text < / span > < / div >, and make the height of the container always less than or equal to the height of the text content or supported by the text content, and then we can set the word number of the container.

Text overflow judgment

Whether the text exceeds n lines, set the fixed font size or automatically adjust the font size, you need to judge whether the accurate text exceeds. There are many methods to judge the overflow. Here, we compare the current height of the text with the maximum height allowed for N lines of text. The current height of the text can be through container The maximum height allowed for N lines of text can be obtained by scrollheight * n (the height of a single line of text is determined by the line height of the text). Finally, compare the two sizes to obtain the text overflow.

/**
 * @param {Number} lineNum Maximum number of text lines allowed
 * @param {html element} containerEle Text container
 * @returns Boolean Returns whether the text overflows the maximum allowed text function
 */
function isTextOverflow(lineNum, containerEle) {
  let isOverflow = false;
  const computedStyle = getComputedStyle(containerEle);

  if (!/^\d+/.test(computedStyle.lineHeight)) {
    throw new Error('container\'s lineHeight must be a exact value!');
  }

  const lineHeight = Number(computedStyle.lineHeight.slice(0, -2));

  if (containerEle.scrollHeight > Math.ceil(lineHeight * lineNum)) {
    isOverflow = true;
  }
  return isOverflow;
}

First, get the scrollHeight and lineHeight through the getComputedStyle method. Note that using this method requires the container's lineHeight to be an explicit value. If the container's lineHeight is omitted or set as a keyword, the specific value cannot be obtained. Finally, return the comparison result of the two.

N lines of text reduce font size to fit container

If the problem becomes a single line of text, it becomes much easier to reduce the font size to adapt to the container. We only need to adjust the font size so that the width of the text content is just equal to the width of the container, that is, we need to get the current text font size fontSize, the width of the current text when it is not folded, textWidth, and the current container width containerWidth, Target font size = fontSize * containerWidth / textWidth.

The simplest and easiest way to get the width of the text without breaking lines is to set the whiteSpace of the container to nowrap, wait for the browser to rearrange and redraw, get the width of the non breaking lines, and then reset the whiteSpace, or create an additional copy element with whiteSpace as nowrap, insert it into the dom, and wait for the browser to rearrange and redraw. In this way, we can easily write single line text and automatically reduce the font size to adapt to the container.

/**
 * @param {*} containerEle Text container
 * @param {*} minFontSize Limit the minimum font size that can be reduced to
 * @returns 
 */
 function adjustFontSizeSingle(containerEle, minFontSize = 8) {
  return new Promise(async (resolve) => {
    if (!isTextOverflow(1, containerEle)) {
      resolve();
      return;
    }

    const computedStyle = getComputedStyle(containerEle);
  
    const needResetWhiteSpace = computedStyle.whiteSpace;
    const needResetOverflow = computedStyle.overflow;
    const fontSize = Number(computedStyle.fontSize.slice(0, -2));
  
    // Set text not to wrap to calculate the total length of the text
    containerEle.style.whiteSpace = 'nowrap';
    containerEle.style.overflow = 'hidden';
  
    await nextTick();
  
    const textBody = containerEle.childNodes[0];
    if (containerEle.offsetWidth < textBody.offsetWidth) {
      // Scale down the font size to just fill the container
      containerEle.style.fontSize = `${Math.max(
        fontSize * (containerEle.offsetWidth / textBody.offsetWidth),
        minFontSize
      )}px`;
    }
  
    containerEle.style.whiteSpace = needResetWhiteSpace;
    containerEle.style.overflow = needResetOverflow;
    resolve();
  });
}

await adjustFontSizeSingle(ele);
console.log('Adjustment complete');


Before adjustment

After adjustment

Presentation

Using the above method requires an additional dom operation. Can we save this dom operation? Our method can be used CanvasRenderingContext2D.measureText() To calculate the width of the text when it is not folded. By setting the same font size and font for the canvas brush, and then calling measureText, you can get the same single line string width as the native dom.

/**
 * @param {*} containerEle Text container
 * @param {*} minFontSize Limit the minimum font size that can be reduced to
 * @param {*} adjustLineHeight Adjust row height proportionally
 * @returns 
 */
async function adjustFontSizeSingle(containerEle, minFontSize = 16, adjustLineHeight = false) {
  let isOverflow = false;
  const computedStyle = getComputedStyle(containerEle);

  if (!/^\d+/.test(computedStyle.lineHeight)) {
    throw new Error('container\'s lineHeight must be a exact value!');
  }

  let lineHeight = Number(computedStyle.lineHeight.slice(0, -2));
  let fontSize = Number(computedStyle.fontSize.slice(0, -2));

  if (isTextOverflow(1, containerEle)) {
    isOverflow = true;
  }

  if (!isOverflow) {
    return;
  }

  const textBody = containerEle.childNodes[0];

  if (!offCtx) {
    offCtx = document.createElement('canvas').getContext('2d');
  }
  const { fontFamily } = computedStyle;
  offCtx.font = `${fontSize}px ${fontFamily}`;
  const { width: measuredWidth } = offCtx.measureText(textBody.innerText);

  if (containerEle.offsetWidth >= measuredWidth) {
    return;
  }

  let firstTransFontSize = fontSize;
  let firstTransLineHeight = lineHeight;
  firstTransFontSize = Math.max(
    minFontSize,
    fontSize * (containerEle.offsetWidth / measuredWidth)
  );
  firstTransLineHeight = firstTransFontSize / fontSize * lineHeight;

  fontSize = firstTransFontSize;
  containerEle.style.fontSize = `${fontSize}px`;
  if (adjustLineHeight) {
    lineHeight = firstTransLineHeight;
    containerEle.style.lineHeight = `${lineHeight}px`;
  }
  console.log('Overflow adjustment completed');
}

Presentation

When the problem rises to multiple lines of text, Perhaps we can imitate the single line text and calculate the ratio between (container width * number of lines) and (total width of non folded lines of the text) to get the proportion of the text that needs to be reduced, but it is not so simple, because the line feed of the text does not simply cut the string equally in each line, but will follow the rules of typesetting line feed. Refer to the specific rules Unicode line breaking algorithm (UAX #14)

Unicode line feed algorithm describes such an algorithm: given the input text, the algorithm will generate a set of positions called break opportunities. Line feed opportunities refer to the line feed allowed here in the process of text rendering, but the actual line feed position needs to be combined with the display window width and font size, which will be confirmed by higher-level application software.

In other words, the text can not wrap in every character. It will wrap in the characters with the latest line breaking opportunity obtained through the algorithm. Of course, the browser also abides by this rule. In addition, it can also define some line breaking rules through css3

  • Line break: used to handle how to break lines of punctuated Chinese, Japanese, or Korean (CJK) text
  • Word break: specifies how to break lines within a word
  • Hyphens: tells the browser how to use hyphens to connect words when wrapping
  • Overflow Wrap: used to indicate whether the browser allows such words to break line breaks to prevent overflow when a string that cannot be separated is too long to fill its package box.

Fortunately, we don't need to calculate the font size exactly enough to fill the container, You can calculate (container width * number of rows) and (the total width of the text without folding lines) is the initial size of the text that needs to be reduced. Thanks to the line feed rule, the text may still overflow, but it is very close to the size that needs to be reduced when the container is just full. By reducing the size in a limited number of cycles, we can get the size we want within a certain error range.

async function adjustFontSizeLoop(lineNum, containerEle, step = 1, minFontSize = 8, adjustLineHeight = false) {
  let isOverflow = false;
  const computedStyle = getComputedStyle(containerEle);

  if (!/^\d+/.test(computedStyle.lineHeight)) {
    throw new Error('container\'s lineHeight must be a exact value!');
  }

  let lineHeight = Number(computedStyle.lineHeight.slice(0, -2));
  let fontSize = Number(computedStyle.fontSize.slice(0, -2));
  if (containerEle.scrollHeight <= Math.ceil(lineHeight * lineNum)) {
    return;
  }

  const textBody = containerEle.childNodes[0];

  if (!offCtx) {
    offCtx = document.createElement('canvas').getContext('2d');
  }
  const { fontFamily } = computedStyle;
  offCtx.font = `${fontSize}px ${fontFamily}`;
  const { width: measuredWidth } = offCtx.measureText(textBody.innerText);
  if (containerEle.offsetWidth * lineNum >= measuredWidth) {
    return;
  }

  let firstTransFontSize = fontSize;
  let firstTransLineHeight = lineHeight;
  firstTransFontSize = Math.max(
    minFontSize,
    fontSize * (containerEle.offsetWidth * lineNum / measuredWidth)
  );
  firstTransLineHeight = firstTransFontSize / fontSize * lineHeight;

  fontSize = firstTransFontSize;
  containerEle.style.fontSize = `${fontSize}px`;
  if (adjustLineHeight) {
    lineHeight = firstTransLineHeight;
    containerEle.style.lineHeight = `${lineHeight}px`;
  }

  if (lineNum === 1) {
    return;
  }
  
  let runTime = 0;
  do {
    await nextTick();
    if (containerEle.scrollHeight > Math.ceil(lineHeight * lineNum)) {
      isOverflow = true;
    } else {
      isOverflow = false;
    }
    if (!isOverflow) {
      break;
    }
    runTime += 1;
    const transFontSize = Math.max(fontSize - step, minFontSize);
    if (adjustLineHeight) {
      lineHeight = this.toFixed(transFontSize / fontSize * lineHeight, 4);
      containerEle.style.lineHeight = `${lineHeight}px`;
    }
    fontSize = transFontSize;
    containerEle.style.fontSize = `${fontSize}px`;
  } while (isOverflow && fontSize > minFontSize);
  console.log('Overflow adjustment completed, Circular setting font size:', runTime, 'second');
}

Presentation

Before adjustment

Adjust to limit 2 line display

next step?

Now it can not solve this problem perfectly (code with uncertain number of cycles is always uncomfortable), and it seems that the efficiency and effect are passable. Can you calculate the font size that needs to be reduced at one time?

We can try to get the information about where the text breaks. Of course, we don't have so much energy to manually implement the line breaking rules, but we can stand on the shoulders of giants and use others to implement good open source libraries: niklasvh/css-line-break.


How to calculate the newline information? Let me think a little more.

Keywords: Javascript Front-end html5

Added by sgiandhu on Mon, 13 Dec 2021 06:11:28 +0200