0

I need a solution to dynamically trim multi-line text in a container and have it end with ellipses instead.

CSS non-solution

Ideally CSS (line clamp) would handle this, but the Browser Support is just not there (85.65%). - Also it's currently an "extremely fragile [...] half-baked non-standardized propert[y]" (source).

JS: Pop until it fits (current solution)

I currently have a working JavaScript solution (measure & pop, until it fits), but it scales poorly. It's affected by the difference between text length and clamped length as well as the number of clamped text elements.

var trimTextAddEllipses = function (targetElementClassName) {
 var elArray = document.getElementsByClassName(targetElementClassName);
 Array.from(elArray).forEach(function (el) {

  // create a cache of full text in data-attribute to be non-destructive
  if (!el.hasAttribute('data-text-cache')) {
   el.setAttribute('data-text-cache',el.textContent);
  }
  // reset
  var words = el.textContent = el.getAttribute('data-text-cache');
  // turn into array to pop off items until it fits.
  var wordArray = words.split(' ');
  while (el.scrollHeight > el.offsetHeight) {
   wordArray.pop();
   el.innerHTML = wordArray.join(' ') + ' …';
  }
 });
};

For resizes I call it as callback to a requestAnimationFrame call to limit the number of times the clamping is done.

JS: Binary search (goal)

The words are in order (i.e. sorted), so a binary search approach would work to make the code more effective.

I found this this binarySearch function but can't come up with a comparison function to make this work properly.

I'd like help coming up with either a comparison function to use with the linked binary search function - or a different binary search solution that works for this problem.

Note

From the comments I'm aware that there's room for further optimization past binary search, but that it would require a lot of JS work (i.e. estimating rather than measuring, thus avoiding rendering this over and over) - but that seems too difficult.

Julix
  • 598
  • 1
  • 9
  • 20
  • 2
    The binarySearch is a good idea because of the O(log(n)) Complexity compared to the O(n) of the loop. It will behave much much better with alot of words. But your real problem will probably be `el.scrollHeight > el.offsetHeight`. By changing the content and calling these properties, you will force a reflow (to calculate the size). See here: https://gist.github.com/paulirish/5d52fb081b3570c81e3a The best approach would probably be to make a test case and use the performance profiler of the browser to optimize it – x4rf41 Mar 06 '19 at 20:28
  • looks like I was right about having an xy problem then... glad I included the full x :) https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem However, it seems that by reducing the number of times the updates need to run the binary search approach still reduces the number of reflows as well. – Julix Mar 06 '19 at 20:57
  • Can't find a better solution for the comparison, because both the width and height of the container fluctuate, so I need to have the JavaScript basically test after reflow "what about now, does it fit?" - unless I'm missing something. Would a shadow DOM have height etc. calculated? – Julix Mar 06 '19 at 21:21
  • 1
    @julix you could get the fonts line height, padding etc. and then estimate the number of words, this is however quite complicated and I'm not sure if that is faster in the end – Jonas Wilms Mar 06 '19 at 21:29
  • @JonasWilms had considered that - figured I'd likely get something wrong and do worse. How often do people resize anyway :D – Julix Mar 06 '19 at 21:37
  • 1
    @Julix You can circumvent the reflow by using the measureText() function of the canvas https://developer.mozilla.org/de/docs/Web/API/CanvasRenderingContext2D/measureText .You would have to calculate line-breaks manually though (by checking how many words fit the given width). For that you could use the binary search approch again. Another general performance improvent would be caching. If the width does not change often, cache the calculated lines, etc. Recalculating the text size every animation frame call will probably never work fine with any method. – x4rf41 Mar 07 '19 at 12:36

1 Answers1

2

You can easily let the loop use binary search:

let end = wordArray.length, distance = end;

while(distance > 0) {
   distance = Math.floor(distance / 2);
   el.innerHTML = wordArray.slice(0, end).join(' ') + '...';
   end += el.scrollHeight > el.offsetHeight ? -distance : distance;
}
Jonas Wilms
  • 132,000
  • 20
  • 149
  • 151
  • 1
    This helps a lot! For an example of 50 words with 35 words fitting in the container, it takes it from 15 steps (all pop) to 8 (distance-end after each step: 25-25, 12-37, 6-31, 3-34, 1-35, 0-35). With more words (or less fitting) this would get even better. – Julix Mar 06 '19 at 21:15
  • 1
    Was wondering how this isn't destroying the wordArray, then I noticed it's slice not splice! Neat! – Julix Mar 06 '19 at 21:37
  • I noticed that it doesn't seem to consider the `...` in it's calculations, so sometimes the `...` is on the next line! - I tested that again with my old code and didn't have the same issue. Any idea why? – Julix Mar 06 '19 at 21:53
  • 1
    @julix Whoops, I guess ut should check the height after changing the content, not before. – Jonas Wilms Mar 07 '19 at 06:29
  • Also there is not really an end condition which you usually have with binary search, therefore the result might by of by one – Jonas Wilms Mar 07 '19 at 06:32
  • This one is right less of the time than the last one was - and both have had off by one errors in either direction. I tried having a code that checks for overflow and then takes off one more word, but since the off by one can be in either direction that didn't consistently work. – Julix Mar 07 '19 at 19:52
  • How is there not an end condition? When it isn't over-flowing and adding any more would make it overflow, stop. – Julix Mar 08 '19 at 19:48
  • 1
    @julix yes, but usually you directly know when you found the element you were looking for, in this case however you won't know it directly. – Jonas Wilms Mar 08 '19 at 21:06