1

Below I've written some code that takes some content in a table cell and truncates it to two lines if it runs over. When trying to find the correct content length that is close to 2 full lines (but does not go over!) I take a logarithmic approach (I think). I first cut the content in half. I then check the content again and either add or subtract a quarter (half of the half). Etc.

Requirements:

  • Ellipsis (...) at the end of truncated text.
  • Responsive, strategy should work for dynamic width cells

Questions:

  • In the snippet, I've included an example that results in 3 lines. How can I guarantee I land at 2 lines while getting reasonably close to 2 full lines?
  • I did the logarithmic approach so I wouldn't have to do something like pop a word, retest, pop a word, retest, etc. This still seems too expensive, how can I improve this?

document.querySelectorAll('.expand').forEach(td => {
  
  // get cell styles
  let styles = window.getComputedStyle(td);
  let lineHeight = parseInt(styles.lineHeight, 10);

  // create test element, mostly because td doesn't support max-height
  let el = document.createElement('div');
  el.innerHTML = td.innerHTML;
  el.style.maxHeight = (lineHeight * 2) + 'px';
  el.style.overflow = 'hidden';
  td.appendChild(el);

  // if scrollHeight is greater than clientHeight, we need to do some expand-y stuff
  if (el.scrollHeight > el.clientHeight) {
    
    // store content
    let content = el.innerHTML.trim(), 
        len = content.length;
    for (let i=Math.round(len*.5);; i=Math.round(i*.5)) {
      let over = el.scrollHeight > el.clientHeight;
      
      // if over 2 lines, cut by half
      // else increase by half
      over ? (len-=i) : (len+=i);
      
      // update innerHTML with updated content
      el.innerHTML = content.slice(0, len);
      
      console.log(i, len);
      
      // break if within margin of 10 and we landed under
      if (i<10 && !over) break;
    }
    
    td.innerHTML = `
      <div class="hide-expanded">${el.innerHTML.slice(0, -3).trim()}...</div>
      <div class="show-expanded">${content}</div>
      <button type="button">Toggle</button>`;
    
    td.querySelector('button').addEventListener('click', e => td.classList.toggle('expanded'))
  }
});
html {
  font-size: 14px;
  line-height: 24px;
  font-family: Helvetica, Arial, sans-serif;
}

table {
  border-collapse: collapse;
}

td {
  white-space: nowrap;
  padding: 1rem;
}

.expand {
  white-space: normal;
}

.expand:not(.expanded) .show-expanded,
.expand.expanded .hide-expanded {
  display: none;
}
<table>
  <tbody>
    <tr>
      <td class="expand">This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content. This is some content.</td>
    </tr>
  </tbody>
</table>
savinger
  • 6,544
  • 9
  • 40
  • 57
  • This is a tough one, I remember I had to do something similar a while back but I just ended up using a jQuery plugin instead, there are a few of them out there, I think I used 'dot dot dot'. Here is the link if you are interested http://dotdotdot.frebsite.nl/ – mojarras Oct 31 '16 at 23:07
  • You may want to look at [Text overflow ellipsis on two lines](http://stackoverflow.com/q/15909489/215552), which uses only CSS for this. – Heretic Monkey Nov 01 '16 at 14:24

1 Answers1

0

This github repo was the best (and most terse) solution I could find. I have adapted a solution from it.

https://github.com/dollarshaveclub/shave/blob/master/src/shave.js

export default function shave(target, maxHeight, opts) {
  if (!maxHeight) throw Error('maxHeight is required');
  let els = typeof target === 'string' ? document.querySelectorAll(target) : target;
  if (!('length' in els)) els = [els];

  const defaults = {
    character: '…',
    classname: 'js-shave',
    spaces: true,
  };
  const character = opts && opts.character || defaults.character;
  const classname = opts && opts.classname || defaults.classname;
  const spaces = opts && opts.spaces === false ? false : defaults.spaces;
  const charHtml = `<span class="js-shave-char">${character}</span>`;

  for (let i = 0; i < els.length; i++) {
    const el = els[i];
    const span = el.querySelector(`.${classname}`);

    // If element text has already been shaved
    if (span) {
      // Remove the ellipsis to recapture the original text
      el.removeChild(el.querySelector('.js-shave-char'));
      el.textContent = el.textContent; // nuke span, recombine text
    }

    // If already short enough, we're done
    if (el.offsetHeight < maxHeight) continue;

    const fullText = el.textContent;
    const words = spaces ? fullText.split(' ') : fullText;

    // If 0 or 1 words, we're done
    if (words.length < 2) continue;

    // Binary search for number of words which can fit in allotted height
    let max = words.length - 1;
    let min = 0;
    let pivot;
    while (min < max) {
      pivot = (min + max + 1) >> 1;
      el.textContent = spaces ? words.slice(0, pivot).join(' ') : words.slice(0, pivot);
      el.insertAdjacentHTML('beforeend', charHtml);
      if (el.offsetHeight > maxHeight) max = spaces ? pivot - 1 : pivot - 2;
      else min = pivot;
    }

    el.textContent = spaces ? words.slice(0, max).join(' ') : words.slice(0, max);
    el.insertAdjacentHTML('beforeend', charHtml);
    const diff = spaces ? words.slice(max + 1).join(' ') : words.slice(max);

    el.insertAdjacentHTML('beforeend',
      `<span class="${classname}" style="display:none;">${diff}</span>`);
  }
}
savinger
  • 6,544
  • 9
  • 40
  • 57