Monday, February 26, 2018

JS, Vue: how to split text based on window height?

Leave a Comment

I have text that I want to split at a specific point so that you dont need to scroll down.

That means I want to know when the text gets longer than the availible height of the window. This needs to be known before I show the text. The problem with that is that I need to know how my layout will look like before I render it, because the whole thing should be responsive to width and height.

I also plan to resize the fontsizes a little. So taking all that to account, does anyone of you know of you know how to split the text at the correct point?

Thank you in advance.

PS: The text is actually an array and is looking like this for e.g.:

text = [{content: Hello, wordID: ..., offsetBegin: 0, offsetEnd: 5,          ...},{content: World!, wordID: ..., offsetBeding: 7, offsetEnd: 12,...}] 

So the only thing I need to know is the indexes on where to split the text so that there are no scrolbars on the main window. Splitting the text can occur more than once too.

The whole thing will start to be displayed and computed in the mounted() hook and will be recomputed each time the 'resize' event of for window is fired.

1 Answers

Answers 1

This questions seems to have the XY problem. You might want to reconsider if this is really how you want to solve the problem.

However, if you really want to get the cutoff point in JS:

The height of the text depends on multiple things, such as the width of the element, the font size and -weight, the kerning, so on and so forth. There are a lot of variables to consider, and it's unlikely that you can do the calculations without rendering anything.

Instead, you should ask the browser to render your element, and then remove it before the rendering is shown to the user. This would be done by forcing a reflow once you have inserted your text, measuring where the text should end, and then forcing a new reflow once you've removed your text.

The tricky part is measuring where the text should end. I would personally solve this by inserting elements at every position I want a cutoff to be possible at (e.g. after every word), and then looping through them to see which overflows the container.


Below is a vanilla JS implementation of the idea. You should be able to fairly easily implement it in Vue.

const DEBUG = true;    const $textInp = document.getElementById("text-inp");  const $target = document.getElementById("target");  const $outp = document.getElementById("outp");    document.getElementById("calc-btn").addEventListener("click", () => {      const text = $textInp.value;      const data = getTextCutoff(text, $target);      $outp.textContent = JSON.stringify(data);      if (!DEBUG) { $target.textContent = text.substr(0, data.end); }  });    /**   * Returns an object of format { end: <Number> }   * Where `end` is the last index which can be displayed inside $elm without overflow   */  function getTextCutoff(text, $elm) {      $elm.innerHTML = ""; // empty the element      const prevOverflow = $elm.style.overflow;      const prevPosition = $elm.style.position;      $elm.style.overflow = "visible";      $elm.style.position = "relative"; // to make sure offsetHeight gives relative to parent      const $indicators = [];      const $nodes = [];      // turn our text into an array of text nodes, with an indicator node after each      let currentIndex = 0;      const words = text.split(" ");      words.forEach(          (word, i) => {              if (i > 0) {                  word = " "+word;              }              currentIndex += word.length;              const $wordNode = document.createTextNode(word);              $nodes.push($wordNode);              const $indicatorNode = document.createElement("span");              $indicatorNode.strIndex = currentIndex;              if (DEBUG) { $indicatorNode.classList.add("text-cutoff-indicator"); }              $indicators.push($indicatorNode);              $nodes.push($indicatorNode);          }      );          // insert our elements      $nodes.forEach($node => $elm.appendChild($node));          // then find the first indicator that is overflown      const maxHeight = $elm.offsetHeight;      let $lastIndicator = $indicators[$indicators.length - 1];      for (let i = 0; i < $indicators.length; ++i) {    	    const $indicator = $indicators[i];          const bottomPos = $indicator.offsetTop + $indicator.offsetHeight;          if (bottomPos > maxHeight) {              break;          } else { $lastIndicator = $indicator; }      }          if (DEBUG) {          $lastIndicator.style.borderColor = "green";          $lastIndicator.classList.add("overflown");      }          // then reset everything - this also forces reflow      if (!DEBUG) { $elm.innerHTML = ""; }      $elm.style.overflow = prevOverflow;      $elm.style.position = prevPosition;          // then return      return {          end: $lastIndicator.strIndex      };  }
#target {    height: 128px;    background: #ddd;  }    .text-cutoff-indicator {    margin-left: -2px;    border-left: 2px solid red;  }    .text-cutoff-indicator.overflown ~ .text-cutoff-indicator {    opacity: 0.5;  }
<div>    <textarea id="text-inp" placeholder="Enter text here"></textarea>    <button id="calc-btn">Apply text</button>    <pre id="outp"></pre>  </div>  <div id="target"></div>

If You Enjoyed This, Take 5 Seconds To Share It

0 comments:

Post a Comment