6

I have this situation :

div { width: 200px }
<div> example example example example example</div>

Text jumps to next line automatically when filling the full width of the <div>.

Using javascript how can I have the rendered content in the above line?

note: In the character string, there is no newline character

expected result from above snippet:

"example example example" corresponding with row 1 and "example example" corresponding with row 2

Kaiido
  • 123,334
  • 13
  • 219
  • 285
Tuan Jihoo
  • 73
  • 1
  • 5

3 Answers3

18

You can make use of the Range API and its handy getBoundingClientRect() method to determine which character marks the seizure in a TextNode.

Note that this obviously needs to be recalculated every time the window is resized / something changes the layout.

function getLineBreaks(node) {
  // we only deal with TextNodes
  if(!node || !node.parentNode || node.nodeType !== 3)
    return [];
  // our Range object form which we'll get the characters positions
  const range = document.createRange();
  // here we'll store all our lines
  const lines = [];
  // begin at the first char
  range.setStart(node, 0);
  // initial position
  let prevBottom = range.getBoundingClientRect().bottom;
  let str = node.textContent;
  let current = 1; // we already got index 0
  let lastFound = 0;
  let bottom = 0;
  // iterate over all characters
  while(current <= str.length) {
    // move our cursor
    range.setStart(node, current);
    if(current < str.length -1)
     range.setEnd(node, current+1);
    bottom = range.getBoundingClientRect().bottom;
    if(bottom > prevBottom) { // line break
      lines.push(
        str.substr(lastFound , current - lastFound) // text content
      );
      prevBottom = bottom;
      lastFound = current;
    }
    current++;
  }
  // push the last line
  lines.push(str.substr(lastFound));

  return lines;
}

console.log(getLineBreaks(document.querySelector('.test').childNodes[0]));
div.test {
  width: 50px;
  margin-bottom: 100px;
  word-break: break-all;
}

body>.as-console-wrapper{max-height:100px}
<div class="test">This is some quite long content that will wrap in multiple lines</div>

And if you need the relative y position of each lines:

function getLineBreaks(node) {
  // we only deal with TextNodes
  if(!node || !node.parentNode || node.nodeType !== 3)
    return [];
  // our Range object form which we'll get the characters positions
  const range = document.createRange();
  // here we'll store all our lines
  const lines = [];
  // begin at the first character
  range.setStart(node, 0);
  // get the position of the parent node so we can have relative positions later
  let contTop = node.parentNode.getBoundingClientRect().top;
  // initial position
  let prevBottom = range.getBoundingClientRect().bottom;
  let str = node.textContent;
  let current = 1; // we already got index 0
  let lastFound = 0;
  let bottom = 0;
  // iterate over all characters
  while(current <= str.length) {
    // move our cursor
    range.setStart(node, current);
    if(current < str.length - 1)
      range.setEnd(node, current+1); // wrap it (for Chrome...)
    bottom = range.getBoundingClientRect().bottom;
    if(bottom > prevBottom) { // line break
      lines.push({
        y: prevBottom - (contTop || 0), // relative bottom
        text: str.substr(lastFound , current - lastFound) // text content
      });
      prevBottom = bottom;
      lastFound = current;
    }
    current++;
  }
  // push the last line
  lines.push({
    y: bottom - (contTop || 0),
    text: str.substr(lastFound)
  });

  return lines;
}

console.log(getLineBreaks(document.querySelector('.test').childNodes[0]));
div.test {
  width: 50px;
  margin-bottom: 100px;
}

body>.as-console-wrapper{max-height:100px}
<div class="test">This is some quite long content that will wrap in multiple lines</div>

For the ones who need it to work over elements instead of a single text-node, here is a rewrite, which may very well fail (e.g with RTL direction) but which should be fine for most cases.

function getLineBreaks(elem) {
  // our Range object form which we'll get the characters positions
  const range = document.createRange();
  // here we'll store all our lines
  const lines = [];
  const nodes = grabTextNodes(elem);
  let left = 0;
  // get the position of the parent node so we can have relative positions later
  let contTop = nodes[0].parentNode.getBoundingClientRect().top;
  // initial position
  let prevLeft = null;
  let lineText = "";
  let startRange = null;
  for (const node of nodes) {
    let nodeText = node.textContent;
    const textLength = nodeText.length;
    let rangeIndex = 0;
    let textIndex = 0;
    while (rangeIndex <= textLength) {
      range.setStart(node, rangeIndex);
      if (rangeIndex < textLength - 1) {
        range.setEnd(node, rangeIndex + 1); // wrap the range (for Chrome...)
      }
      left = range.getBoundingClientRect().right;
      if (prevLeft === null) { // first pass
        prevLeft = left;
        startRange = range.cloneRange();
      } else if (left < prevLeft) { // line break
        // store the current line content
        lineText += nodeText.slice(0, textIndex);
        startRange.setEnd(range.endContainer, range.endOffset);
        const {
          bottom
        } = startRange.getBoundingClientRect();
        lines.push({
          y: bottom - contTop,
          text: lineText
        });
        // start a new line
        prevLeft = left;
        lineText = "";
        nodeText = nodeText.slice(textIndex);
        textIndex = 0;
        startRange = range.cloneRange();
      }
      rangeIndex++;
      textIndex++;
      prevLeft = left;
    }
    // add the remaining text from this node into the current line content
    lineText += nodeText;
  }
  // push the last line
  startRange.setEnd(range.endContainer, range.endOffset);
  const { bottom } = startRange.getBoundingClientRect();
  lines.push({
    y: bottom - contTop,
    text: lineText
  });
  return lines;
}

console.log(getLineBreaks(document.querySelector('.test')));

function grabTextNodes(elem) {
  const walker = document.createTreeWalker(elem, NodeFilter.SHOW_TEXT, null);
  const nodes = [];
  while (walker.nextNode()) {
    nodes.push(walker.currentNode);
  }
  return nodes;
}
div.test {
  width: 150px;
  margin-bottom: 100px;
}

.red {
  color: red;
}
<div class="test"><span class="red">This</span> is some quite long content that will wrap in <span class="red">mutiple</span> lines..</div>
Kaiido
  • 123,334
  • 13
  • 219
  • 285
  • thanks for the code. but in the fiddle, it doesn't work when i change the .red css class to also have font-size: 2em – icube Jan 10 '22 at 18:47
  • @icube, yes, using `.bottom` in this case was not very wise, the `.red` elements don't share the same `.bottom` value since they are bigger. I did update my answer to include a new version that looks at `.left` instead, but note that this is still not bullet proof. Oh and if you want it as a fiddle: https://jsfiddle.net/puk067ym/ – Kaiido Jan 11 '22 at 03:28
  • bravo such clever use of these APIs, so nicely written/commented. this little function will become a prized (&& fully attributed) module in my personal collection. many thnx – Nick Briz Nov 27 '22 at 16:03
-1

Try CSS

div {
  width:200px;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
Razana N.
  • 70
  • 5
  • 4
    They want to retrieve the content of rendered text after line breaks are applied by CSS, they don't want to know how to do the linebreaks. – Kaiido Apr 10 '19 at 05:24
-1

This can be done using CSS. No Javascript required.

<div> example example example example example</div>
<style>
div{
    width: 200px;
    word-wrap: break-word;
}
</style>
Rahul Malu
  • 556
  • 2
  • 9
  • 6
    They want to retrieve the content of rendered text after line breaks are applied by CSS, they don't want to know how to do the linebreaks. – Kaiido Apr 10 '19 at 05:25