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>

2
  • 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 dotdotdot.frebsite.nl Commented Oct 31, 2016 at 23:07
  • You may want to look at Text overflow ellipsis on two lines, which uses only CSS for this. Commented Nov 1, 2016 at 14:24

1 Answer 1

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>`); } } 
Sign up to request clarification or add additional context in comments.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.